carriage return remove

carriage return remove

(:
 Copyright 2002-2016 MarkLogic Corporation.  All Rights Reserved.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
:)
xquery version "1.0-ml";

(: This module supports code generation for entity services.
 : This module has no public API.  See entity-services.xqy for
 : the public interface.
 :)
module namespace es-codegen = "http://marklogic.com/entity-services-codegen";

import module namespace esi = "http://marklogic.com/entity-services-impl"
    at "entity-services-impl.xqy";

import module namespace functx   = "http://www.functx.com" at "/MarkLogic/functx/functx-1.0-nodoc-2007-01.xqy";

declare namespace es = "http://marklogic.com/entity-services";
declare namespace tde = "http://marklogic.com/xdmp/tde";
declare namespace xq = "http://www.w3.org/2012/xquery";

declare option xdmp:mapping "false";
declare option xq:require-feature "xdmp:three-one";

declare private function es-codegen:casting-function-name(
    $datatype as xs:string
) as xs:string
{
    if ($datatype eq "iri")
    then "sem:iri"
    else "xs:" || $datatype
};

declare private function es-codegen:comment(
    $comment-text as xs:string*
) as xs:string
{
    concat('    (: ', string-join($comment-text, "
       "), '  :)
')
};

declare private function es-codegen:extraction-for(
    $prefix as xs:string,
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string
{
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := if (empty(map:get($entity-type, "required")))
        then ()
        else json:array-values( map:get($entity-type, "required") )
    let $is-required := $property-name =
            ( map:get($entity-type, "primaryKey"), $required-properties )
    let $is-array :=
            map:get(map:get($properties, $property-name), "datatype")
            eq "array"
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name := es-codegen:casting-function-name($property-datatype)
    let $wrap-if-array := function($str, $fn, $arrity-ok) {
            if ($is-array and $is-required)
            then concat("json:to-array(", $str, " ! ", $fn, "(.) )")
            else
            if ($is-array)
            then concat("es:extract-array(", $str, ", ", $fn, if ($arrity-ok) then "#1" else (), ")")
            else concat($fn, "(", $str, ")")
        }
    let $ref :=
        if ($is-array)
        then $properties=>map:get($property-name)=>map:get("items")=>map:get("$ref")
        else $properties=>map:get($property-name)=>map:get("$ref")
    let $path-to-property := concat("$source-node/", $property-name)
    let $property-comment :=
        if (empty($ref))
        then ""
        else if (contains($ref, "#/definitions"))
        then es-codegen:comment("The following property is a local reference.")
        else
        es-codegen:comment((
            'The following property assigment comes from an external reference.',
            'Its generated value probably requires developer attention.'))
    let $ref-name := functx:substring-after-last($ref, "/")
    let $extract-reference-fn :=
            concat("function($path) { json:object()",
                      "    =>map:with('$type', '", $ref-name, "')",
                      "    =>map:with('$ref', $path/", $ref-name, "/text() ) }")
    let $value :=
        if (empty($ref))
        then
            $wrap-if-array($path-to-property, $casting-function-name, true())
        else
            if (contains($ref, "#/definitions"))
            then
                $wrap-if-array($path-to-property, concat($prefix, ":extract-instance-", $ref-name), true())
            else
                $wrap-if-array($path-to-property, $extract-reference-fn, false())

    (: if a property is required, use map:with to force inclusion :)
    let $function-call-string :=
        if ($is-required)
        then "    =>   map:with("
        else "    =>es:optional("
    return
        fn:concat( $property-comment,
               $function-call-string,
               functx:pad-string-to-length("'" || $property-name || "',", " ", max((  (string-length($property-name)+4), 25) )+1 ),
               $value,
               ")")
};

declare function es-codegen:instance-converter-generate(
    $model as map:map
) as document-node()
{
    let $info := map:get($model, "info")
    let $title := map:get($info, "title")
    let $prefix := lower-case(substring($title,1,1)) || substring($title,2)
    let $version:= map:get($info, "version")
    let $base-uri := esi:resolve-base-uri($info)
    return
document {
<module>xquery version "1.0-ml";

(:
 This module was generated by MarkLogic Entity Services.
 The source entity type document was {$title}-{$version}
 To use this module, examine how you wish to extract data from sources,
 and modify the various extract-instance-{{X}} functions to
 create the instances you wish.

 You may wish to/need to alter
 1.  values.  For example, creating duration values from decimal months.
 2.  references.  This conversion module assumes you want to denormalize
     instances when storing them in documents.  You may choose to remove
     code that denormalizes, and just include reference values in your
     instances instead.
 3.  Source XPath expressions.  The data coming into the extract-instance-{{X}}
     functions will probably not be exactly what this module predicts.

 After modifying this file, put it in your project for deployment to the modules
 database of your application, and check it into your source control system.

 Modification History:
 Generated at timestamp: {fn:current-dateTime()}
 Persisted by AUTHOR
 Date: DATE
 :)

module namespace {$prefix}
    = "{$base-uri}{$title}-{$version}";

import module namespace es = "http://marklogic.com/entity-services"
    at "/MarkLogic/entity-services/entity-services.xqy";

declare option xdmp:mapping "false";

(:
  extract-instance-{{entity-type}} functions

  These functions together take a source document and create a nested
  map structure from it.
  The resulting map is used by instance-to-canonical-xml to create documents
  in the database.

  It is expected that an implementer will edit at least XPath expressions in
  the extraction functions.  It is less likely that you will want to edit
  the instance-to-canonical-xml or envelope functions.
 :)

{
    for $entity-type-name in map:keys(map:get($model, "definitions"))
    return
    <extract-instance>
(:~
 : Creates a map:map instance from some source document.
 : @param $source-node  A document or node that contains
 :   data for populating a {$entity-type-name}
 : @return A map:map instance with extracted data and
 :   metadata about the instance.
 :)
declare function {$prefix}:extract-instance-{$entity-type-name}(
    $source as node()?
) as map:map
{{
    let $source-node := {$prefix}:init-source($source, '{$entity-type-name}')

    let $instance := json:object()
    (: If desired, add the original source document as an attachment. :)
        =>{$prefix}:add-attachments($source-node, $source)
    (: Add type information to the entity instance (required, do not change) :)
        =>map:with('$type', '{ $entity-type-name }')

    return

    (: The following logic may not be required for every extraction       :)
    (: if this $source-node has no child elements, it has a reference key :)
    if (empty($source-node/*))
    then $instance=>map:with('$ref', $source-node/text())
    (: Otherwise, this source node contains instance data. Populate it. :)
    else

    (:
    The following code populates the properties of the '{$entity-type-name}'
    entity type. Ensure that all of the property paths are correct for your
    source data.  The general pattern is
    =>map:with('keyName', casting-function($source-node/path/to/data))
    but you may also wish to convert values
    =>map:with('dateKeyName',
          xdmp:parse-dateTime("[Y0001]-[M01]-[D01]T[h01]:[m01]:[s01].[f1][Z]",
          $source-node/path/to/data/in/the/source))
    You can also implement lookup functions,
    =>map:with('lookupKey',
          cts:search( collection('customers'),
              string($source-node/path/to/lookup/key))/id
    or populate the instance with constants.
    =>map:with('constantValue', 10)
    The output of this function should structurally match the output of
    es:model-get-test-instances($model)
    :)

    $instance
{
    (: Begin code generation block :)
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $value-lines :=
        for $property-name in map:keys($properties)
        return es-codegen:extraction-for($prefix, $model, $entity-type-name, $property-name)
    return fn:string-join($value-lines, "
")
        (: end code generation block :)
    }
}};
</extract-instance>/text()
}

(:~
 : Turns an entity instance into an XML structure.
 : This out-of-the box implementation traverses a map structure
 : and turns it deterministically into an XML tree.
 : Using this function as-is should be sufficient for most use
 : cases, and will play well with other generated artifacts.
 : @param $entity-instance A map:map instance returned from one of the extract-instance
 :    functions.
 : @return An XML element that encodes the instance.
 :)
declare function {$prefix}:instance-to-canonical-xml(
    $entity-instance as map:map
) as element()
{{
    (: Construct an element that is named the same as the Entity Type :)
    element {{ map:get($entity-instance, "$type") }}  {{
        if ( map:contains($entity-instance, "$ref") )
        then map:get($entity-instance, "$ref")
        else
            for $key in map:keys($entity-instance)
            let $instance-property := map:get($entity-instance, $key)
            where ($key castable as xs:NCName)
            return
                typeswitch ($instance-property)
                (: This branch handles embedded objects.  You can choose to prune
                   an entity's representation of extend it with lookups here. :)
                case json:object+
                    return
                        for $prop in $instance-property
                        return element {{ $key }} {{ {$prefix}:instance-to-canonical-xml($prop) }}
                (: An array can also treated as multiple elements :)
                case json:array
                    return
                        for $val in json:array-values($instance-property)
                        return
                            if ($val instance of json:object)
                            then element {{ $key }} {{
                                attribute datatype {{ "array" }},
                                {$prefix}:instance-to-canonical-xml($val) }}
                            else element {{ $key }} {{
                                attribute datatype {{ "array" }},
                                $val }}
                (: A sequence of values should be simply treated as multiple elements :)
                case item()+
                    return
                        for $val in $instance-property
                        return element {{ $key }} {{ $val }}
                default return element {{ $key }} {{ $instance-property }}
    }}
}};

(:
 : Wraps a canonical instance (returned by instance-to-canonical-xml())
 : within an envelope patterned document, along with the source
 : document, which is stored in an attachments section.
 : @param $entity-instance an instance, as returned by an extract-instance
 : function
 : @return A document which wraps both the canonical instance and source docs.
 :)
declare function {$prefix}:instance-to-envelope(
    $entity-instance as map:map
) as document-node()
{{
    document {{
        element es:envelope {{
            element es:instance {{
                element es:info {{
                    element es:title {{ map:get($entity-instance,'$type') }},
                    element es:version {{ "{$version}" }}
                }},
                {$prefix}:instance-to-canonical-xml($entity-instance)
            }},
            element es:attachments {{
                map:get($entity-instance, "$attachments")
            }}
        }}
    }}
}};

declare private function {$prefix}:init-source(
    $source as node()*,
    $entity-type-name as xs:string
) as node()*
{{
    if ( ($source instance of document-node())
        or (exists
            ($source/element()[fn:node-name(.) eq xs:QName($entity-type-name)] )))
    then $source/node()
    else $source
}};

declare private function {$prefix}:add-attachments(
    $instance as json:object,
    $source-node as node()*,
    $source as node()*
) as json:object
{{
    $instance
    =>map:with('$attachments',
        typeswitch($source-node)
        case object-node() return xdmp:quote($source)
        case array-node() return xdmp:quote($source)
        default return $source)
}};

</module>/text()
}

};

declare private function es-codegen:value-for-conversion(
    $source-model as map:map,
    $target-model as map:map,
    $target-entity-type-name as xs:string,
    $target-property-name as xs:string,
    $display-property-name as xs:string,
    $extraction-functions as map:map
) as xs:string
{
    let $target-info := map:get($target-model, "info")
    let $target-title := map:get($target-info, "title")
    let $target-prefix := lower-case(substring($target-title,1,1)) || substring($target-title,2)
    let $source-info := map:get($source-model, "info")
    let $source-title := map:get($source-info, "title")
    let $source-prefix := lower-case(substring($source-title,1,1)) || substring($source-title,2)
    let $module-prefix := $target-prefix || "-from-" || $source-prefix

    let $target-entity-type := $target-model
        =>map:get("definitions")
        =>map:get($target-entity-type-name)
    let $target-property := $target-entity-type
        =>map:get("properties")
        =>map:get($target-property-name)
    let $source-entity-type := $source-model
        =>map:get("definitions")
        =>map:get($target-entity-type-name)    (: this function is only called with matching types/props :)
    let $source-properties :=
        if (exists($source-entity-type))
        then $source-entity-type=>map:get("properties")
        else ()
    let $is-missing-source :=
        (exists($source-properties) and not($target-property-name = map:keys($source-properties)))
    let $source-correlate :=
        if (exists($source-properties))
        then map:get($source-properties, $target-property-name)
        else ()
    let $target-is-array :=
        if (exists($target-property))
        then $target-property=>map:get("datatype") eq 'array'
        else false()
    let $source-is-array :=
        exists($source-correlate) and $source-correlate=>map:get("datatype") eq 'array'
    let $target-ref :=
        if (exists($target-property))
        then
            if ($target-is-array)
            then $target-property=>map:get("items")=>map:get("$ref")
            else $target-property=>map:get("$ref")
        else ()
    let $source-ref :=
        if ($source-is-array)
        then $source-correlate=>map:get("items")=>map:get("$ref")
        else
        if (exists($source-correlate))
        then $source-correlate=>map:get("$ref")
        else ()
    let $is-scalar-from-ref := empty($target-ref) and exists($source-ref)
    let $target-is-scalar-array := $target-is-array and empty($target-ref)
         and map:get($target-property, "items")=>map:contains("datatype")
    let $source-is-scalar-array := $source-is-array and empty($source-ref)
        and map:get($source-correlate, "items")=>map:contains("datatype")
    let $properties-correlate := not($target-is-scalar-array) and not($source-is-scalar-array)
    let $is-array-from-scalar := $target-is-scalar-array and not($source-is-scalar-array)
    let $is-array-from-array := $target-is-scalar-array and $source-is-scalar-array
    let $truncates-array := not($target-is-array) and $source-is-scalar-array
    let $is-scalar-from-array := empty($target-ref) and $truncates-array

    let $target-datatype := esi:resolve-datatype($target-model, $target-entity-type-name, $target-property-name)
    let $casting-function-name := es-codegen:casting-function-name($target-datatype)
    let $required-properties := if (empty(map:get($target-entity-type, "required")))
        then ()
        else json:array-values( map:get($target-entity-type, "required") )
    let $is-required := $target-property-name =
            ( map:get($target-entity-type, "primaryKey"), $required-properties )
    let $path-to-property := concat("$source-node/", $target-property-name)
    let $target-ref-name := functx:substring-after-last($target-ref, "/")
    let $source-ref-name := functx:substring-after-last($source-ref, "/")
    (: warning: property path override if source is ref and target is scalar :)
    let $path-to-property := if ($is-scalar-from-ref)
                            then $path-to-property || "/" || $source-ref-name
                            else $path-to-property
    let $wrap-if-array := function($str, $fn, $arrity-ok) {
            if ($target-is-array and $is-required)
            then concat("json:to-array(", $str, " ! ", $fn, "(.) )")
            else
            if ($target-is-array)
            then concat("es:extract-array(", $str, ", ", $fn, if ($arrity-ok) then "#1" else (), ")")
            else concat($fn, "(", $str, ")")
        }
    let $is-reference-from-scalar-or-array :=
        exists($target-ref) and empty($source-ref)

    let $extract-scalar-fn :=
            concat("function($path) { json:object()",
                      "=>map:with('$type', '", $target-ref-name, "')",
                      "=>map:with('$ref', ", $casting-function-name, "(($path)[1]) ) }")

    let $comment :=
        if ($is-missing-source)
        then es-codegen:comment((
                "The following property was missing from the source type.",
                "The XPath will not up-convert without intervention."))
        else if ($truncates-array)
        then es-codegen:comment("Warning: potential data loss, truncated array.")
        else ""

    let $extract-reference-fn :=
        map:put($extraction-functions, $target-ref-name,
            fn:string-join(
                (
                concat('    let $extract-reference-',$target-ref-name,' := '),
                if (contains($target-ref, "#/definitions"))
                then
                    ("        function($path) { ",
                     "         if ($path/*)",
                     concat("         then ", $module-prefix, ":convert-instance-", $target-ref-name, "($path)"),
                     "         else ",
                     "           json:object()",
                     concat("           =>map:with('$type', '", $target-ref-name, "')"),
                     "           =>map:with('$ref', $path/text() ) ",
                     "        }"
                    )
                else
                    ("        function($path) { ",
                     concat("         json:object()=>map:with('$type', '", $target-ref-name, "')"),
                     "         =>map:with('$ref', $path/text() ) ",
                     "        }")
                ),
            "
")
        )

    let $function-call-string :=
        if ($is-required)
        then "    =>   map:with("
        else "    =>es:optional("
    let $property-padding :=
        functx:pad-string-to-length("'" || $display-property-name || "',", " ", max((  (string-length($target-property-name)+4), 25) )+1 )
    let $value :=
        if ($is-scalar-from-array)
        then concat($casting-function-name, "( fn:head(", $path-to-property, ") )")
        else if (empty($target-ref))
        then $wrap-if-array($path-to-property, $casting-function-name, true())
        else if ($is-reference-from-scalar-or-array)
        then $wrap-if-array($path-to-property, $extract-scalar-fn, true())
        else $wrap-if-array($path-to-property || "/*", "$extract-reference-" || $target-ref-name, false() )

    return
        fn:string-join( ($comment,
            $function-call-string,
            $property-padding,
            $value,
            ")
")
          )
};

declare function es-codegen:version-translator-generate(
    $source-model as map:map,
    $target-model as map:map
) as document-node()
{
    let $target-info := map:get($target-model, "info")
    let $target-title := map:get($target-info, "title")
    let $target-prefix := lower-case(substring($target-title,1,1)) || substring($target-title,2)
    let $target-version:= map:get($target-info, "version")
    let $target-definitions := map:get($target-model, "definitions")
    let $target-entity-type-names := map:keys($target-definitions)
    let $target-base-uri := esi:resolve-base-uri($target-info)

    let $source-info := map:get($source-model, "info")
    let $source-title := map:get($source-info, "title")
    let $source-prefix := lower-case(substring($source-title,1,1)) || substring($source-title,2)
    let $source-version:= map:get($source-info, "version")
    let $source-definitions := map:get($source-model, "definitions")
    let $source-base-uri := esi:resolve-base-uri($source-info)

    let $module-prefix := $target-prefix || "-from-" || $source-prefix
    let $module-namespace := concat(
        $target-base-uri,
        $target-title,
        "-" ,
        $target-version,
        "-from-",
        $source-title,
        "-",
        $source-version)
    let $target-info := json:object()

(: BEGIN convert instance block :)
    let $convert-instance :=
        for $entity-type-name in $target-entity-type-names
        let $info-map := json:object()
        let $_ := map:put($target-info, $entity-type-name, $info-map)
        return
    <convert-instance>
(:~
 : Creates a map:map instance representation of the target
 : entity type {$entity-type-name} from an envelope document
 : containing a source entity instance, that is, instance data
 : of type {$entity-type-name}, version {$source-version}.
 : @param $source  An Entity Services envelope document (<es:envelope>)
 :  or a canonical XML instance of type {$entity-type-name}.
 : @return A map:map instance that holds the data for {$entity-type-name},
 :  version {$target-version}.
 :)
{ if (not($entity-type-name = map:keys($source-definitions)))
    then "
(: Type " || $entity-type-name || " is not in the source model.
 : XPath expressions are created as though there were no change between source and target type.
 :)"
    else () }
declare function {$module-prefix}:convert-instance-{$entity-type-name}(
    $source as node()
) as map:map
{{
    let $source-node := {$module-prefix}:init-source($source, '{$entity-type-name}')

{
    (: Begin code generation block :)
    let $extraction-functions := json:object()
    let $entity-type := map:get($target-definitions, $entity-type-name)
    let $source-entity-type :=
            if (map:contains($source-definitions, $entity-type-name))
            then map:get($source-definitions, $entity-type-name)
            else map:put($info-map, "missing from source model.", ())
    let $ts-func :=
        function($value) {
            typeswitch($value)
            case empty-sequence() return "None"
            case json:array return fn:string-join(json:array-values($value), ", ")
            default return $value
        }
    let $compare :=
        function($property) {
            (
            $ts-func(map:get($entity-type, $property)),
            if (exists($source-entity-type) and map:contains($source-entity-type, $property))
            then "( in source: " || $ts-func(map:get($source-entity-type, $property)) || " )"
            else "( in source: None )"
            )
        }
    let $_ :=
            (
                map:put($info-map, "primaryKey: ", $compare("primaryKey")),
                map:put($info-map, "required: ", $compare("required")),
                map:put($info-map, "range indexes: ", $compare("rangeIndex")),
                map:put($info-map, "word lexicons: ", $compare("wordLexicon"))
            )
    let $properties := map:get($entity-type, "properties")
    let $values :=
        for $property-name in map:keys($properties)
        return
            es-codegen:value-for-conversion($source-model,
                $target-model,
                $entity-type-name,
                $property-name,
                $property-name,
                $extraction-functions)
    let $missing-properties :=
        if (exists($source-entity-type))
        then
            for $property-name in map:keys(map:get($source-entity-type, "properties"))
            where not($property-name = map:keys(map:get($entity-type, "properties")))
                return
                    es-codegen:value-for-conversion($source-model, $target-model, $entity-type-name, $property-name, "NO TARGET", map:map())
        else ()
    return
        fn:concat(
            fn:string-join(
                for $k in map:keys($extraction-functions)
                where $k ne ""
                return map:get($extraction-functions, $k), "
"),
'    return
    json:object()
    (: If the source is an envelope or part of an envelope document,
     : copies attachments to the target
     :)
    =>',$module-prefix,':copy-attachments($source-node)
    (: The following line identifies the type of this instance.  Do not change it. :)
    =>map:with("$type", "', $entity-type-name, '")
    (: The following lines are generated from the "',$entity-type-name,'" entity type. :)',
            fn:string-join($values),
            if (exists($missing-properties))
            then
                es-codegen:comment(fn:string-join(
                    ("The following properties are in the source, but not the target:
",
                    $missing-properties)))
            else ()
            )
    (: end code generation block :)
    }
}};
    </convert-instance>
(: END convert instance block :)

    let $removed-type :=
        (: Make comments for removed ET types :)
        for $removed-entity-type-name in map:keys($source-definitions)
        let $removed-entity-type := map:get($source-definitions, $removed-entity-type-name)
        where not( $removed-entity-type-name = $target-entity-type-names)
        return
<removed-type>
(:
 Entity type {$removed-entity-type-name} is in source document
 but not in target document.
 The following XPath expressions should get values from the source
 instances but there is no specified target.
 This comment can be as a starting point for writing a custom
 version converter.

declare function {$module-prefix}:convert-instance-{$removed-entity-type-name}(
    $source-node as node()
) as map:map
{{
    json:object()
    (: If the source is an envelope or part of an envelope document,
     : copies attachments to the target
     :)
    =>{$module-prefix}:copy-attachments($source-node)
    =>map:with('$type', '{ $removed-entity-type-name }')
{

map:put($target-info, $removed-entity-type-name, map:entry("", "Removed Type")),
let $values :=
    for $removed-property-name in $removed-entity-type=>map:get("properties")=>map:keys()
    return es-codegen:extraction-for($module-prefix, $source-model, $removed-entity-type-name, $removed-property-name)
return fn:string-join($values, "
")
}
:)
</removed-type>

    return document {
<module>xquery version "1.0-ml";
module namespace {$module-prefix}
    = "{$module-namespace}";

import module namespace es = "http://marklogic.com/entity-services"
    at "/MarkLogic/entity-services/entity-services.xqy";

declare option xdmp:mapping "false";

(:
 This module was generated by MarkLogic Entity Services.
 Its purpose is to create instances of entity types
 defined in
 {$target-title || ", version " || $target-version}
 from documents that were persisted according to model
 {$source-title || ", version " || $source-version}

 Modification History:
 Generated at timestamp: {fn:current-dateTime()}
 Persisted by AUTHOR
 Date: DATE

 Target Model {$target-title || "-" || $target-version} Info:

 {
map:keys($target-info) !
    function($et) {
        let $et-report := map:get($target-info, $et)
        return
        fn:concat("Type ", $et, ":
",
            fn:string-join(
               map:keys($et-report) !
                    function($report-key) {
                        "    ",
                        $report-key,
                        fn:string-join(map:get($et-report, $report-key), ", "),
                        "
"
                    }(.)
            )
        ),
        "
"
   }(.)
}:)

{
(: this flowr makes order deterministic, which is better for dev UX. :)
for $c in $convert-instance order by $c return $c/text()}

{for $r in $removed-type order by $r return $r/text()}

declare private function {$module-prefix}:init-source(
    $source as node()*,
    $entity-type-name as xs:string
) as node()*
{{
    if ( ($source//es:instance/element()[node-name(.) eq xs:QName($entity-type-name)]))
    then $source//es:instance/element()[node-name(.) eq xs:QName($entity-type-name)]
    else $source
}};

declare private function {$module-prefix}:copy-attachments(
    $instance as json:object,
    $source as node()*
) as json:object
{{
    $instance
    =>es:optional('$attachments',
        $source ! fn:root(.)/es:envelope/es:attachments/node())
}};

</module>/text()
    }
};


Refactored map BOS construction to return sequence of topicref/resource pair maps

(: =====================================================

   DITA Link Management Utilities

   Utilities that support general DITA link management
   actions.

   Author: W. Eliot Kimber

   Copyright (c) 2015 DITA For Small Teams
   Licensed under Apache License 2

   ===================================================== :)

module namespace lmutil="http://dita-for-small-teams.org/xquery/modules/linkmgmt-utils";

declare namespace dfst="http://dita-for-small-teams.org";

import module namespace df="http://dita-for-small-teams.org/xquery/modules/dita-utils";
import module namespace bxutil="http://dita-for-small-teams.org/xquery/modules/basex-utils";
import module namespace dfstcnst="http://dita-for-small-teams.org/xquery/modules/dfst-constants";
import module namespace relpath="http://dita-for-small-teams.org/xquery/modules/relpath-utils";

declare variable $lmutil:useMatchParamsLocalMaps :=
                               map{'linktype' :'topicref',
                                   'format' : 'ditamap',
                                   'scope' : 'local'
                                  };
declare variable $lmutil:useMatchParamsPeerMaps :=
                               map{'linktype' :'topicref',
                                   'format' : 'ditamap',
                                   'scope' : 'peer'
                                  };

(:~
 : Find all links that use direct URI references to their target resources.
 : Returns list of link item maps
 :
 : Each link item map has the following items:
 :
 : 'link' : The element that is the link
 : 'rootMap' : The root map document that defines the namespace the link
 :             is resolved in.
 : 'resolvedMap' : The fully-resolved map that defines key space the key
 :                 reference is resolved in.
 : 'keySpace' : The constructed key space for the root map.
 : 'processingRole' : For topicrefs, the @processing-role value. For
 :                    other links, 'normal'.
 : 'linkContext': The relevant container type for the link, one of:
 :                - 'title' : Link occurs within a title element
 :                - 'prolog' : Link occurs with topicmeta (maps) or prolog (topics)
 :                - 'shortdesc' : Link occurs within a short description
 :                - 'abstract' : Link occurs within an abstract (but not within a
 :                               short description within an abstract)
 :                - 'body' : Link occurs within a topic body
 :                - 'reltable' : Link is a topicref within a relationship table.
 :                - 'navtree' : Link is a topicref with @processing-role of 'normal' and
 :                              is not within a relationship table
 :                - 'resources' : Link is a topicref with a processing role of "resource-only".
 :  'isDirect' : - 'true' if link is a direct URI reference
 :               - 'false' if link uses a resolvable key reference
 :
 : For direct links the link element is sufficient
 : to allow resolution as use context has no affect on how the link is resolved.
 : For "." ("this topic") fragment IDs, the context is always the containing topic.
 :
 : @param dbName Name of the database that contains the documents to be processed.
 : @param logID ID of the log to put log messages in.
 :)
declare function lmutil:findAllDirectLinks($dbName) as map(*)* {
  let $db := db:open($dbName)
  (: First do direct URI references, which don't require any
     context knowledge:
   :)
  let $links := collection($dbName)//*[df:isTopicRef(.) and not(@keyref)] |
                collection($dbName)//*[contains(@class, ' topic/xref ') and (@href and not(@keyref))] |
                collection($dbName)//*[contains(@class, ' topic/data-about ') and (@href and not(@keyref))] |
                collection($dbName)//*[contains(@class, ' topic/longdescref ') and (@href and not(@keyref))] |
                collection($dbName)//*[@conref]
   (: Construct a link item map for each link element :)
   for $link in $links
       return map{'link': $link,
                  'rootMap': (),
                  'resolvedMap': (),
                  'keySpace': (),
                  'processingRole':
                     if (df:isTopicRef($link))
                        then if ($link/@processing-role)
                                then string($link/@processing-role)
                                else 'normal' (: Default for topicref :)
                        else 'normal', (: non-topicref links :)
                  'linkContext': lmutil:getLinkContext($link)
                 }
};

(: Give a document, finds all references to that document that match the
   type of uses as configured in the $useParams.

   @param doc Document to find uses of.
   @param useParams Use filter parameters. Only uses that match the parameters
                    will be reported.
   @return Zero or more use record elements.

   The use parameters are:

   linktype: List of base type names (e.g., 'topicref, xref) or qualified
             class names (e.g., 'map-d/navref') of the types of links
             to report usage for. If unspecifed, all link types are reported.
             The keyword "#conref" indicates uses via @conref or @conkeyref

   format:   List of @format values by which the document is used, e.g. "dita", "ditamap",
             etc. If unspecified, uses are not filtered by @format value.

   scope:    List of @scope values to filter the uses by. If unspecified, uses are not
             filtered by @scope value.

   TBD: Need for direct/indirect filter, other filters.

   FIXME: Need to rationalize the coding pattern for use parameters and
          their use by the useRecordMatcher() function. The code is a little
          confused right now as a result of some quick refactoring.

 :)
declare function lmutil:getUses($elem as element(), $useParams) as element()* {

   let $linktypes := if ($useParams('linktype'))
                        then $useParams('linktype')
                        else ('#any')
   let $formats   := if ($useParams('format'))
                        then $useParams('format')
                        else ('#any')
   let $scopes   := if ($useParams('scope'))
                        then $useParams('scope')
                        else ('#any')

   (: Note that all references are ultimately to elements,
      so resource keys are always for elements, not
      documents. Given an element we can always get its
      containing document.

      In DITA, except for <dita> documents, references to
      documents with no fragment identifier are implicitly
      to the root elements of those documents (i.e., a map
      or topic element).

      The resource key here is used as the name of the
      directory that contains the where-used records for
      this element.
    :)
   let $resKey := lmutil:constructResourceKeyForElement($elem)

   (: Now find all use records for the resource key that match the filter
      specification.

      :)
    let $dbName := bxutil:getMetadataDbNameForDoc(root($elem))
    let $collection := $dbName || $dfstcnst:where-used-dir || '/' || $resKey
    let $records := collection($collection)
                       /dfst:useRecord[lmutil:useRecordMatcher(., $linktypes, $formats, $scopes)]
    return $records

};

(: Given an element with an @id value, construct the unique resource key for it.
   For a given element the resource ID is guaranteed to be unique within
   a snapshot (git commit).

   @param elem Element to get resource ID for.
   @returns Resource key string.

   The resource key is a combination of the absolute URI of the containing
   document and the element's tree position within the document, producing
   a unique key for any element in any document on a given snapshot (version
   in time). Resource keys are not reliably unique across snapshots as the position of
   the element within its containing document could change from version to
   version.

   The resource key is used to look up the element in where-used records, either
   as the element used or the element doing the use (links).

 :)
declare function lmutil:constructResourceKeyForElement($elem as element()) as xs:string {
  let $targetDocHash := hash:md5(document-uri(root($elem)))
  let $treepos := for $anc in ($elem/ancestor-or-self::*)
                      return string(count($anc | $anc/preceding-sibling::*))
  let $key := string-join($treepos, '.')
  return $targetDocHash || '^' || $key

};

(:~
 : Determines if a given where-used record matches the filter
 : specified parameters.
 :
 : FIXME: Use a map to pass the filter parameters rather than
 : individual arguments.
 :)
declare function lmutil:useRecordMatcher($record as element(),
                                         $linktypes as xs:string*,
                                         $formats as xs:string*,
                                         $scopes as xs:string*) as xs:boolean {
   let $result := ((if ($linktypes = '#any')
                       then true()
                       else string($record/@linkType) = $linktypes) and
                   (if ($formats = '#any')
                       then true()
                       else string($record/@format) = $formats) and
                   (if ($scopes = '#any')
                       then true()
                       else string($record/@scope) = $scopes))
  return $result
};

(:~
 : Given a link element, determines the linkContext value for it.
 :)
declare function lmutil:getLinkContext($link as element()) as xs:string {
  if (df:class($link, 'map/topicref'))
     then if ($link/ancestor::*[df:class(., 'map/reltable')])
             then 'reltable'
          else if ($link/@processing-role = ('resource-only'))
               then 'resources'
          else 'navtree'
     else if ($link/ancestor::*[df:class(., 'topic/title')])
          then 'title'
     else if ($link/ancestor::*[df:class(., 'topic/shortdesc')])
          then 'shortdesc'
     else if ($link/ancestor::*[df:class(., 'topic/abstract')])
          then 'abstract'
     else if ($link/ancestor::*[df:class(., 'topic/prolog') or df:class(., 'map/topicmeta')])
          then 'prolog'
     else if ($link/ancestor::*[df:class(., 'topic/body')])
          then 'body'
     else 'unknown-context'

};

(:~
 : Given a link item map, attempts to resolve the link to an element (in DITA a given link
 : can address at most one element).
 :
 : Returns a map with the following members:
 : 'target': A sequence of zero or more elements addressed
 :           by the link.
 : 'log':    A sequence of zero or more log entry elements
 :           generated by the resolution attempt.
 :)
declare function lmutil:resolveDirectLink($linkItem as map(*)) as map(*) {

   let $link as element()? := $linkItem('link')
   return if ($link)
      then
       let $resultMap := if (df:class($link, 'map/topicref') and ($link/@href))
                            then df:resolveTopicRef($link)
                            else df:resolveNonTopicRefDirectLink($link)

       let $targets := $resultMap('target')
       let $log := (<info>Link: {
                   concat('<',
                          name($link), ' ',
                          lmutil:reportAtts($link, ('href', 'keyref', 'keys', 'conref', 'scope')),
                          '>')} [class: "{string($link/@class)}"], doc: "{document-uri(root($link))}"</info>,
                  if ($targets)
                     then <info>  Link resolved</info>
                     else $resultMap('log')
                  )
       return map{'target' : $targets, 'log' : $log, 'link' : $linkItem}
     else
       map{'target' : (),
                 'log' : (),
                 'link' : $linkItem
                }
};

(: Given a link descriptor map and the database that contains it, attempts
   to resolve the link to an element (in DITA a given link
   can address at most one element).

   Returns a map with the following members:
   'target': A sequence of zero or more elements addressed
             by the link.
   'log':    A sequence of zero or more log entry elements
             generated by the resolution attempt.
   'link':   The input link item.
 :)
declare function lmutil:resolveIndirectLink($linkItem as map(*)) as map(*) {

   (: FIXME: Implement this function :)
   let $targets := ()
   let $log := ()
   return map{'target' : $targets, 'log' : $log , 'link' : $linkItem}
};

(:~
 : Given a database, finds all the indirect links. Returns a map
 : the the members:
 : 'links' : A sequence of maps, where each map represents one link.
 : 'log' : A sequence of log entry elements.
 :
 : Each link map has the following items:
 :
 : 'link' : The element that is the link
 : 'rootMap' : The root map document that defines the namespace the link
 :             is resolved in.
 : 'resolvedMap' : The fully-resolved map that defines key space the key
 :                 reference is resolved in.
 : 'keySpace' : The constructed key space for the root map.
 :
 :)
declare function lmutil:findAllIndirectLinks($dbName) as map(*)* {
   (:

1. Find all root maps (maps with no local-scope topicref references or
   maps with peer topicref references). This requires that the direct-reference
   use records are up to date. It also requires that the resolved map and key space
   documents have already been created.

2. For each root map, Walk the resolved map, creating link items for each key-based topicref.
   Resolve topicrefs to topics and process each topic to create link items for each key-based
   link in each topic.

   :)

   let $rootMaps as element()* := lmutil:getRootMaps($dbName)

   let $linksFromTopics :=
       for $map in $rootMaps
           let $resolvedMap := lmutil:getResolvedMapForMap($map)
           let $bos as map(*)* := lmutil:getMapBoundedObjectSet($resolvedMap, ('dita'))
           return for $member in $bos
              return () (:lmm:getIndirectLinks:)

    let $links := $linksFromTopics

(:   let $links := map{ 'link' : (),
                      'rootMap' : (),
                      'resolvedMap' : (),
                      'keySpace' : ()
                    }
:)
   return $links
};

(:~
 : Given a map resolves all the topicrefs to their local-scope resources
 : and returns the set of "BOS members", where each member is a map
 : with the topicref that pointed to the topic and the resolved topic.
 : The topicref is required in order to have the map context, which is required
 : in order to determine the key space within which any key references within the
 : topic are resolved. Note that a BOS member is a unique topicref/resource pair.
 : If the same resource is referenced multiple times in the map there will be one BOS member
 : for each reference.
 :
 : @param map Map to process. Does not resolve submap references, so if
 : you want the resources for a map tree supply the resolved map.
 :
 : @param formats A list of one or more @format values to return in the set.
 : The special value '#non-dita' matches any @format value that is not 'dita'
 : or 'dita-map'. Specify ('dita') to return only topic members of
 : the BOS. Non-DITA resources are represented by the topicrefs that point to
 : them. If not specified, all local-scope resources are returned.
 : @return Sequence of maps, one for each topicref/resource pair
 :)
declare function lmutil:getMapBoundedObjectSet(
                            $map as element(),
                            $formats as xs:string*) as map(*)* {
  let $topicrefs := $map//*[df:isTopicRef(.)]
                           [df:isLocalScope(.)]
                           [not($formats) or
                            df:getEffectiveFormat(.) = $formats]
  let $members :=
      for $topicref in $topicrefs
          let $resource := if (df:getEffectiveFormat($topicref) = ('dita'))
             then lmutil:resolveTopicRefFromResolvedMap($topicref)
             else $topicref
          return map { 'topicref' : $topicref,
                       'resource' : $resource }
  return $members

};

(:~
 : Resolves a topicref from a resolved map. Uses the additional metadata
 : in the resolved map to determine the target document that contains
 : the element.
 :)
declare function lmutil:resolveTopicRefFromResolvedMap($topicref as element())
                                                                    as element()? {
  let $baseURI := string(($topicref/ancestor::*[@origMapURI]/@origMapURI)[1])
  let $targetURI := string($topicref/@href)
  let $resolvedURI := relpath:resolveURI($targetURI, $baseURI)
  let $result := doc($resolvedURI)/*
  return $result
};

(:~
 : Find maps that either have no local-scope topicrefs to them or that have
 : any peer-scope topicrefs to them.
 :
 : @param dbName Name of the content database that holds the map to examine.
 : @return List, possibly empty, of root maps.
 :)
declare function lmutil:getRootMaps($dbName as xs:string) as element()* {
  let $candMaps := collection($dbName)/*[df:class(., 'map/map')]
  let $result :=
      for $candMap in $candMaps
          return if (lmutil:isRootMap($candMap))
                   then $candMap
                   else ()
  return $result
};

(:~
 : Returns true if the specified map element is a root map.
 :
 : Note that there are many ways that root mapness could be determined.
 : The logic used here is any map that has no direct map local-scope references
 : or any peer-scope references. This is based on the presumption that a root
 : map would not normally be useable as local-scope submap. DITA 1.3's new
 : meaning for scope="peer" on topicrefs to maps explicitly means "the referenced
 : map is a root map".
 :
 : Other options would be to have a master map that uses peer map references to
 : explicitly identify root maps and use only that to determine rootness, to
 : maintain some sort of metadata that indicates rootness, use a specific map
 : specialization or map specialization plus descendant element configuration
 : (e.g., bookmaps that have booktitle elements are root maps, all others are
 : not).
 :
 : @param map Map to check for rootness
 : @return true() if the map is a root map.
 :)
declare function lmutil:isRootMap($map as element()) as xs:boolean {
  let $result := ((lmutil:getRefCount($map, $lmutil:useMatchParamsLocalMaps) = 0) or
                  (lmutil:getRefCount($map, $lmutil:useMatchParamsPeerMaps) > 0))
  return $result
};

(:~
 : Gets the number of references to the specified element that match the specified
 : match criteria.
 :
 : @param elem Element to get reference count for.
 : @param matcher Use record matcher to use for selecting references
 : @return Number of matches
 :)
declare function lmutil:getRefCount($elem as element(),
                                    $matchParams as map(*)) as xs:integer {
  let $uses := lmutil:getUses($elem, $matchParams)
  let $result := count($uses)
  return $result
};

(: Construct a string report of the listed attributes :)
declare function lmutil:reportAtts($elem as element(), $attNames) as xs:string {
   let $result := for $att in $elem/@*
                      return if (name($att) = $attNames)
                                then concat(name($att), '="', string($att), '"')
                                else ()
   return string-join($result, ' ')
};

(:~
 : Get the resolved map document for a map. The resolved map must have already
 : been constructed (XQuery doesn't let use do updating actions wherever we
 : want).
 :
 : @param map Content map to get the resolved map for.
 : @return the resolved map or an empty sequence if there is no resolved map
 : (for example, because the map is not a root map).
 :)
declare function lmutil:getResolvedMapForMap($map as element()) as element()? {
  let $resolvedMapURI := lmutil:getResolvedMapURIForMap($map)
  let $metadataDbName := bxutil:getMetadataDbNameForDoc(root($map))

  let $resolvedMap := collection($metadataDbName || $resolvedMapURI)
  return $resolvedMap
};

(:~
 : Construct the database URI to use for a resolved map.
 :
 : @param map DITA map element to get the URI for
 : @returns The URI of the resolved map as a string
 :)
declare function lmutil:getResolvedMapURIForMap(
                        $map as element()) as xs:string {
  let $mapDocHash := hash:md5(document-uri(root($map)))
  return $dfstcnst:resolved-map-dir ||
         "/" || $mapDocHash || ".ditamap"
};

(: End of Module :)

(: =====================================================

   DITA Link Management Utilities

   Utilities that support general DITA link management
   actions.

   Author: W. Eliot Kimber

   Copyright (c) 2015 DITA For Small Teams
   Licensed under Apache License 2

   ===================================================== :)

module namespace lmutil="http://dita-for-small-teams.org/xquery/modules/linkmgmt-utils";

declare namespace dfst="http://dita-for-small-teams.org";

import module namespace df="http://dita-for-small-teams.org/xquery/modules/dita-utils";
import module namespace bxutil="http://dita-for-small-teams.org/xquery/modules/basex-utils";
import module namespace dfstcnst="http://dita-for-small-teams.org/xquery/modules/dfst-constants";
import module namespace relpath="http://dita-for-small-teams.org/xquery/modules/relpath-utils";

declare variable $lmutil:useMatchParamsLocalMaps :=
                               map{'linktype' :'topicref',
                                   'format' : 'ditamap',
                                   'scope' : 'local'
                                  };
declare variable $lmutil:useMatchParamsPeerMaps :=
                               map{'linktype' :'topicref',
                                   'format' : 'ditamap',
                                   'scope' : 'peer'
                                  };

(:~
 : Find all links that use direct URI references to their target resources.
 : Returns list of link item maps
 :
 : Each link item map has the following items:
 :
 : 'link' : The element that is the link
 : 'rootMap' : The root map document that defines the namespace the link
 :             is resolved in.
 : 'resolvedMap' : The fully-resolved map that defines key space the key
 :                 reference is resolved in.
 : 'keySpace' : The constructed key space for the root map.
 : 'processingRole' : For topicrefs, the @processing-role value. For
 :                    other links, 'normal'.
 : 'linkContext': The relevant container type for the link, one of:
 :                - 'title' : Link occurs within a title element
 :                - 'prolog' : Link occurs with topicmeta (maps) or prolog (topics)
 :                - 'shortdesc' : Link occurs within a short description
 :                - 'abstract' : Link occurs within an abstract (but not within a
 :                               short description within an abstract)
 :                - 'body' : Link occurs within a topic body
 :                - 'reltable' : Link is a topicref within a relationship table.
 :                - 'navtree' : Link is a topicref with @processing-role of 'normal' and
 :                              is not within a relationship table
 :                - 'resources' : Link is a topicref with a processing role of "resource-only".
 :  'isDirect' : - 'true' if link is a direct URI reference
 :               - 'false' if link uses a resolvable key reference
 :
 : For direct links the link element is sufficient
 : to allow resolution as use context has no affect on how the link is resolved.
 : For "." ("this topic") fragment IDs, the context is always the containing topic.
 :
 : @param dbName Name of the database that contains the documents to be processed.
 : @param logID ID of the log to put log messages in.
 :)
declare function lmutil:findAllDirectLinks($dbName) as map(*)* {
  let $db := db:open($dbName)
  (: First do direct URI references, which don't require any
     context knowledge:
   :)
  let $links := collection($dbName)//*[df:isTopicRef(.) and not(@keyref)] |
                collection($dbName)//*[contains(@class, ' topic/xref ') and (@href and not(@keyref))] |
                collection($dbName)//*[contains(@class, ' topic/data-about ') and (@href and not(@keyref))] |
                collection($dbName)//*[contains(@class, ' topic/longdescref ') and (@href and not(@keyref))] |
                collection($dbName)//*[@conref]
   (: Construct a link item map for each link element :)
   for $link in $links
       return map{'link': $link,
                  'rootMap': (),
                  'resolvedMap': (),
                  'keySpace': (),
                  'processingRole':
                     if (df:isTopicRef($link))
                        then if ($link/@processing-role)
                                then string($link/@processing-role)
                                else 'normal' (: Default for topicref :)
                        else 'normal', (: non-topicref links :)
                  'linkContext': lmutil:getLinkContext($link)
                 }
};

(: Give a document, finds all references to that document that match the
   type of uses as configured in the $useParams.

   @param doc Document to find uses of.
   @param useParams Use filter parameters. Only uses that match the parameters
                    will be reported.
   @return Zero or more use record elements.

   The use parameters are:

   linktype: List of base type names (e.g., 'topicref, xref) or qualified
             class names (e.g., 'map-d/navref') of the types of links
             to report usage for. If unspecifed, all link types are reported.
             The keyword "#conref" indicates uses via @conref or @conkeyref

   format:   List of @format values by which the document is used, e.g. "dita", "ditamap",
             etc. If unspecified, uses are not filtered by @format value.

   scope:    List of @scope values to filter the uses by. If unspecified, uses are not
             filtered by @scope value.

   TBD: Need for direct/indirect filter, other filters.

   FIXME: Need to rationalize the coding pattern for use parameters and
          their use by the useRecordMatcher() function. The code is a little
          confused right now as a result of some quick refactoring.

 :)
declare function lmutil:getUses($elem as element(), $useParams) as element()* {

   let $linktypes := if ($useParams('linktype'))
                        then $useParams('linktype')
                        else ('#any')
   let $formats   := if ($useParams('format'))
                        then $useParams('format')
                        else ('#any')
   let $scopes   := if ($useParams('scope'))
                        then $useParams('scope')
                        else ('#any')

   (: Note that all references are ultimately to elements,
      so resource keys are always for elements, not
      documents. Given an element we can always get its
      containing document.

      In DITA, except for <dita> documents, references to
      documents with no fragment identifier are implicitly
      to the root elements of those documents (i.e., a map
      or topic element).

      The resource key here is used as the name of the
      directory that contains the where-used records for
      this element.
    :)
   let $resKey := lmutil:constructResourceKeyForElement($elem)

   (: Now find all use records for the resource key that match the filter
      specification.

      :)
    let $dbName := bxutil:getMetadataDbNameForDoc(root($elem))
    let $collection := $dbName || $dfstcnst:where-used-dir || '/' || $resKey
    let $records := collection($collection)
                       /dfst:useRecord[lmutil:useRecordMatcher(., $linktypes, $formats, $scopes)]
    return $records

};

(: Given an element with an @id value, construct the unique resource key for it.
   For a given element the resource ID is guaranteed to be unique within
   a snapshot (git commit).

   @param elem Element to get resource ID for.
   @returns Resource key string.

   The resource key is a combination of the absolute URI of the containing
   document and the element's tree position within the document, producing
   a unique key for any element in any document on a given snapshot (version
   in time). Resource keys are not reliably unique across snapshots as the position of
   the element within its containing document could change from version to
   version.

   The resource key is used to look up the element in where-used records, either
   as the element used or the element doing the use (links).

 :)
declare function lmutil:constructResourceKeyForElement($elem as element()) as xs:string {
  let $targetDocHash := hash:md5(document-uri(root($elem)))
  let $treepos := for $anc in ($elem/ancestor-or-self::*)
                      return string(count($anc | $anc/preceding-sibling::*))
  let $key := string-join($treepos, '.')
  return $targetDocHash || '^' || $key

};

(:~
 : Determines if a given where-used record matches the filter
 : specified parameters.
 :
 : FIXME: Use a map to pass the filter parameters rather than
 : individual arguments.
 :)
declare function lmutil:useRecordMatcher($record as element(),
                                         $linktypes as xs:string*,
                                         $formats as xs:string*,
                                         $scopes as xs:string*) as xs:boolean {
   let $result := ((if ($linktypes = '#any')
                       then true()
                       else string($record/@linkType) = $linktypes) and
                   (if ($formats = '#any')
                       then true()
                       else string($record/@format) = $formats) and
                   (if ($scopes = '#any')
                       then true()
                       else string($record/@scope) = $scopes))
  return $result
};

(:~
 : Given a link element, determines the linkContext value for it.
 :)
declare function lmutil:getLinkContext($link as element()) as xs:string {
  if (df:class($link, 'map/topicref'))
     then if ($link/ancestor::*[df:class(., 'map/reltable')])
             then 'reltable'
          else if ($link/@processing-role = ('resource-only'))
               then 'resources'
          else 'navtree'
     else if ($link/ancestor::*[df:class(., 'topic/title')])
          then 'title'
     else if ($link/ancestor::*[df:class(., 'topic/shortdesc')])
          then 'shortdesc'
     else if ($link/ancestor::*[df:class(., 'topic/abstract')])
          then 'abstract'
     else if ($link/ancestor::*[df:class(., 'topic/prolog') or df:class(., 'map/topicmeta')])
          then 'prolog'
     else if ($link/ancestor::*[df:class(., 'topic/body')])
          then 'body'
     else 'unknown-context'

};

(:~
 : Given a link item map, attempts to resolve the link to an element (in DITA a given link
 : can address at most one element).
 :
 : Returns a map with the following members:
 : 'target': A sequence of zero or more elements addressed
 :           by the link.
 : 'log':    A sequence of zero or more log entry elements
 :           generated by the resolution attempt.
 :)
declare function lmutil:resolveDirectLink($linkItem as map(*)) as map(*) {

   let $link as element()? := $linkItem('link')
   return if ($link)
      then
       let $resultMap := if (df:class($link, 'map/topicref') and ($link/@href))
                            then df:resolveTopicRef($link)
                            else df:resolveNonTopicRefDirectLink($link)

       let $targets := $resultMap('target')
       let $log := (<info>Link: {
                   concat('<',
                          name($link), ' ',
                          lmutil:reportAtts($link, ('href', 'keyref', 'keys', 'conref', 'scope')),
                          '>')} [class: "{string($link/@class)}"], doc: "{document-uri(root($link))}"</info>,
                  if ($targets)
                     then <info>  Link resolved</info>
                     else $resultMap('log')
                  )
       return map{'target' : $targets, 'log' : $log, 'link' : $linkItem}
     else
       map{'target' : (),
                 'log' : (),
                 'link' : $linkItem
                }
};

(: Given a link descriptor map and the database that contains it, attempts
   to resolve the link to an element (in DITA a given link
   can address at most one element).

   Returns a map with the following members:
   'target': A sequence of zero or more elements addressed
             by the link.
   'log':    A sequence of zero or more log entry elements
             generated by the resolution attempt.
   'link':   The input link item.
 :)
declare function lmutil:resolveIndirectLink($linkItem as map(*)) as map(*) {

   (: FIXME: Implement this function :)
   let $targets := ()
   let $log := ()
   return map{'target' : $targets, 'log' : $log , 'link' : $linkItem}
};

(:~
 : Given a database, finds all the indirect links. Returns a map
 : the the members:
 : 'links' : A sequence of maps, where each map represents one link.
 : 'log' : A sequence of log entry elements.
 :
 : Each link map has the following items:
 :
 : 'link' : The element that is the link
 : 'rootMap' : The root map document that defines the namespace the link
 :             is resolved in.
 : 'resolvedMap' : The fully-resolved map that defines key space the key
 :                 reference is resolved in.
 : 'keySpace' : The constructed key space for the root map.
 :
 :)
declare function lmutil:findAllIndirectLinks($dbName) as map(*)* {
   (:

1. Find all root maps (maps with no local-scope topicref references or
   maps with peer topicref references). This requires that the direct-reference
   use records are up to date. It also requires that the resolved map and key space
   documents have already been created.

2. For each root map, Walk the resolved map, creating link items for each key-based topicref.
   Resolve topicrefs to topics and process each topic to create link items for each key-based
   link in each topic.

   :)

   let $rootMaps as element()* := lmutil:getRootMaps($dbName)

   let $linksFromTopics :=
       for $map in $rootMaps
           let $resolvedMap := lmutil:getResolvedMapForMap($map)
           let $topics := lmutil:getMapBoundedObjectSet($resolvedMap, ('dita'))
           return for $topic in $topics
              return () (:lmm:getIndirectLinks:)

    let $links := $linksFromTopics

(:   let $links := map{ 'link' : (),
                      'rootMap' : (),
                      'resolvedMap' : (),
                      'keySpace' : ()
                    }
:)
   return $links
};

(:~
 : Given a map resolves all the topicrefs to their local-scope resources
 : and returns the set (a "bounded object set" or BOS).
 :
 : @param map Map to process. Does not resolve submap references, so if
 : you want the resources for a map tree supply the resolved map.
 :
 : @param formats A list of one or more @format values to return in the set.
 : The special value '#non-dita' matches any @format value that is not 'dita'
 : or 'dita-map'. Specify ('dita') to return only topic members of
 : the BOS. Non-DITA resources are represented by the topicrefs that point to
 : them. If not specified, all local-scope resources are returned.
 : @return Set of elements in the bounded object set.
 :)
declare function lmutil:getMapBoundedObjectSet(
                            $map as element(),
                            $formats as xs:string*) as element()* {
  let $topicrefs := $map//*[df:isTopicRef(.)]
                           [df:isLocalScope(.)]
                           [not($formats) or
                            df:getEffectiveFormat(.) = $formats]
  let $members :=
      for $topicref in $topicrefs
          return if (df:getEffectiveFormat($topicref) = ('dita'))
             then lmutil:resolveTopicRefFromResolvedMap($topicref)
             else $topicref
  return $members

};

(:~
 : Resolves a topicref from a resolved map. Uses the additional metadata
 : in the resolved map to determine the target document that contains
 : the element.
 :)
declare function lmutil:resolveTopicRefFromResolvedMap($topicref as element())
                                                                    as element()? {
  let $baseURI := string(($topicref/ancestor::*[@origMapURI]/@origMapURI)[1])
  let $targetURI := string($topicref/@href)
  let $resolvedURI := relpath:resolveURI($targetURI, $baseURI)
  let $result := doc($resolvedURI)/*
  return $result
};

(:~
 : Find maps that either have no local-scope topicrefs to them or that have
 : any peer-scope topicrefs to them.
 :
 : @param dbName Name of the content database that holds the map to examine.
 : @return List, possibly empty, of root maps.
 :)
declare function lmutil:getRootMaps($dbName as xs:string) as element()* {
  let $candMaps := collection($dbName)/*[df:class(., 'map/map')]
  let $result :=
      for $candMap in $candMaps
          return if (lmutil:isRootMap($candMap))
                   then $candMap
                   else ()
  return $result
};

(:~
 : Returns true if the specified map element is a root map.
 :
 : Note that there are many ways that root mapness could be determined.
 : The logic used here is any map that has no direct map local-scope references
 : or any peer-scope references. This is based on the presumption that a root
 : map would not normally be useable as local-scope submap. DITA 1.3's new
 : meaning for scope="peer" on topicrefs to maps explicitly means "the referenced
 : map is a root map".
 :
 : Other options would be to have a master map that uses peer map references to
 : explicitly identify root maps and use only that to determine rootness, to
 : maintain some sort of metadata that indicates rootness, use a specific map
 : specialization or map specialization plus descendant element configuration
 : (e.g., bookmaps that have booktitle elements are root maps, all others are
 : not).
 :
 : @param map Map to check for rootness
 : @return true() if the map is a root map.
 :)
declare function lmutil:isRootMap($map as element()) as xs:boolean {
  let $result := ((lmutil:getRefCount($map, $lmutil:useMatchParamsLocalMaps) = 0) or
                  (lmutil:getRefCount($map, $lmutil:useMatchParamsPeerMaps) > 0))
  return $result
};

(:~
 : Gets the number of references to the specified element that match the specified
 : match criteria.
 :
 : @param elem Element to get reference count for.
 : @param matcher Use record matcher to use for selecting references
 : @return Number of matches
 :)
declare function lmutil:getRefCount($elem as element(),
                                    $matchParams as map(*)) as xs:integer {
  let $uses := lmutil:getUses($elem, $matchParams)
  let $result := count($uses)
  return $result
};

(: Construct a string report of the listed attributes :)
declare function lmutil:reportAtts($elem as element(), $attNames) as xs:string {
   let $result := for $att in $elem/@*
                      return if (name($att) = $attNames)
                                then concat(name($att), '="', string($att), '"')
                                else ()
   return string-join($result, ' ')
};

(:~
 : Get the resolved map document for a map. The resolved map must have already
 : been constructed (XQuery doesn't let use do updating actions wherever we
 : want).
 :
 : @param map Content map to get the resolved map for.
 : @return the resolved map or an empty sequence if there is no resolved map
 : (for example, because the map is not a root map).
 :)
declare function lmutil:getResolvedMapForMap($map as element()) as element()? {
  let $resolvedMapURI := lmutil:getResolvedMapURIForMap($map)
  let $metadataDbName := bxutil:getMetadataDbNameForDoc(root($map))

  let $resolvedMap := collection($metadataDbName || $resolvedMapURI)
  return $resolvedMap
};

(:~
 : Construct the database URI to use for a resolved map.
 :
 : @param map DITA map element to get the URI for
 : @returns The URI of the resolved map as a string
 :)
declare function lmutil:getResolvedMapURIForMap(
                        $map as element()) as xs:string {
  let $mapDocHash := hash:md5(document-uri(root($map)))
  return $dfstcnst:resolved-map-dir ||
         "/" || $mapDocHash || ".ditamap"
};

(: End of Module :)