Including 'attachments' for input flows

Including 'attachments' for input flows

(:
  Copyright 2012-2018 MarkLogic Corporation

  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";

module namespace service = "http://marklogic.com/rest-api/extensions/scaffold-content";

import module namespace consts = "http://marklogic.com/data-hub/consts"
at "/data-hub/4/impl/consts.xqy";

import module namespace debug = "http://marklogic.com/data-hub/debug"
at "/data-hub/4/impl/debug-lib.xqy";

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

import module namespace hent = "http://marklogic.com/data-hub/hub-entities"
at "/data-hub/4/impl/hub-entities.xqy";

import module namespace perf = "http://marklogic.com/data-hub/perflog-lib"
at "/data-hub/4/impl/perflog-lib.xqy";

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

declare namespace es = "http://marklogic.com/entity-services";

declare namespace rapi = "http://marklogic.com/rest-api";

declare option xdmp:mapping "false";

declare function service:comment-xqy($comment-text as xs:string*)
{
  "(: " || fn:string-join($comment-text, "
     ") || " :)"
};

declare function service:comment-sjs($comment-text as xs:string*)
{
  "/* " || fn:string-join($comment-text, "
     ") || " */"
};

declare function service:kebob-case($str as xs:string)
{
  fn:lower-case(fn:replace(fn:replace($str, "([A-Z])", "-$1"), "^-", ""))
};

declare function service:camel-case($str as xs:string)
{
  fn:string-join(
    let $tokens := fn:tokenize($str, "[-_]")
    return
      (
        $tokens[1],
        for $x in fn:subsequence($tokens, 2)
        return
          functx:capitalize-first($x)
      ),
    "")
};

declare function service:get-property($root as xs:string, $property as xs:string)
{
  if (fn:contains($property, "-")) then
    $root || "['" || $property || "']"
  else
    $root || "." || $property
};

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

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

declare function service:generate-lets($model as map:map, $entity-type-name, $mapping as map:map?, $parent-entity as xs:string)
{
  fn:string-join(
    let $definitions := map:get($model, "definitions")
    let $entity-type := map:get($definitions, $entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := (
      map:get($entity-type, "primaryKey"),
      map:get($entity-type, "required") ! json:array-values(.)
    )
    for $property-name in map:keys($properties)
    let $is-required := $property-name = $required-properties
    let $property := map:get($properties, $property-name)
    let $is-array := map:get($property, "datatype") eq "array"
    let $property := map:get($properties, $property-name)
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name :=
      if (map:contains($property, "datatype") and map:get($property, "datatype") ne "array") then
        service:casting-function-name-xqy($property-datatype)
      else ()
    let $wrap-if-array := function($str) {
      if ($is-array) then
        "json:to-array(" || $str || ")"
      else
        $str
    }
    let $ref :=
      if ($is-array) then
        let $items as map:map? := map:get($property, "items")
        return
          $items ! map:get(., "$ref")
      else
        map:get($property, "$ref")
    let $path-to-property := fn:concat("$source-node/", $property-name)
    let $property-comment :=
      if (fn:empty($ref)) then ()
      else if (fn:contains($ref, "#/definitions")) then
        service:comment-xqy("The following property is a local reference.")
      else (
          service:comment-xqy((
            '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 :=
      fn:string-join((
        "",
        "    plugin:make-reference-object('" || $ref-name || "', (: put your value here :) '')"
      ), "
")
    let $value :=
      if (empty($ref)) then (
        if(service:mapping-present($mapping, $property-name) and $parent-entity eq $entity-type-name)
        then (fn:concat("$source",service:map-value($property-name, $mapping)))
        else ($wrap-if-array(fn:concat("$source/", $property-name))))
      else if (contains($ref, "#/definitions")) then
        let $inner-var := "$" || fn:lower-case($ref-name) || "s"
        let $inner-val :=
          if(service:mapping-present($mapping, $property-name))
          then (fn:concat("$source",service:map-value($property-name, $mapping)))
          else (fn:concat("$source/", $property-name))
        return
          fn:string-join((
            "",
            "let " || $inner-var || " :=",
            "  (: create a sequence of " || $ref-name || " instances from your data :)",
            "  for $sub-entity in ("||$inner-val||")",
            "  return",
            "    plugin:extract-instance-" || $ref-name || "($sub-entity)",
            "return",
            if ($is-required) then
              "  json:to-array(" || $inner-var || ")"
            else (
              "  if (fn:exists(" || $inner-var || ")) then",
              "    json:to-array(" || $inner-var || ")",
              "  else ()"
            )
          ), "
    ") || "
"
      else if(fn:not(empty($ref))) then
           fn:concat("$source/", $property-name)
      else $wrap-if-array($extract-reference-fn)

    return (
      $property-comment ! ("", .),

      let $name := service:kebob-case($property-name)
      let $valueWrap := fn:concat(fn:string-join(
        if ($casting-function-name) then
          $casting-function-name else ())
        , fn:concat(
          "(", $value,")"))
      return
        "let $" || $name || " := " || $valueWrap
    )
    , "
  ")
(: end code generation block :)
};

declare function service:generate-xqy($entity as xs:string, $flow-type as xs:string, $model as map:map, $mapping as map:map?)
{
  let $root-name :=
    if ($flow-type eq $consts:INPUT_FLOW) then "$raw-content"
    else "$doc"
  return
    document {
<module>xquery version "1.0-ml";

module namespace plugin = "http://marklogic.com/data-hub/plugins";

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

declare option xdmp:mapping "false";

(:~
: Create Content Plugin
:
: @param $id          - the identifier returned by the collector
: @param $options     - a map containing options. Options are sent from Java
:
: @return - your transformed content
:)
declare function plugin:create-content(
  $id as xs:string,
  {
    if ($flow-type eq $consts:INPUT_FLOW) then
      "$raw-content as node()?,
  "
    else ()
  }$options as map:map) as map:map
{{
  {
    if ($flow-type eq $consts:HARMONIZE_FLOW) then
      "let $doc := fn:doc($id)
  "
    else ()
  }let $source := {$root-name}
  return
  {
    "plugin:extract-instance-" || $entity || "($source)"
  }
}};
  {
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 plugin:extract-instance-{$entity-type-name}(
$source as node()?
) as map:map
{{
{
  if ($entity-type-name eq $entity) then
  <txt>
  (: the original source documents :)
  let $attachments := $source
  let $source      :=
    if ($source/*:envelope and $source/node() instance of element()) then
      $source/*:envelope/*:instance/node()
    else if ($source/*:envelope) then
      $source/*:envelope/*:instance
    else if ($source/instance) then
      $source/instance
    else
      $source
  </txt>/text()
  else (<txt>
  let $source :=
    if($source/node() instance of element()) then
    $source/node()
  else (
    $source
  )
  </txt>/text()
  )
}
{
  if ($entity eq $entity-type-name and fn:empty($mapping) eq fn:false() and fn:empty(map:get($mapping, "name")) eq fn:false()) then
  <txt>(: These mappings were generated using mapping: {map:get($mapping, "name")}, version: {map:get($mapping, "version")} on {fn:current-dateTime()}. :)
  </txt>/text()
  else ()
}
{
service:generate-lets($model, $entity-type-name, $mapping, $entity)
}

  (: return the in-memory instance :)
  (: using the XQuery 3.0 syntax... :)
  let $model := json:object()
  let $_ := (
  {
    if ($entity-type-name eq $entity) then
      "  map:put($model, '$attachments', $attachments),"
    else ()
  }
    map:put($model, '$type', '{ $entity-type-name }'),
    map:put($model, '$version', '{ map:get(map:get($model, "info"), "version") }'){
  let $definitions := map:get($model, "definitions")
  let $entity-type := map:get($definitions, $entity-type-name)
  let $properties := map:get($entity-type, "properties")
  let $property-keys := map:keys($properties)
  where fn:count($property-keys) > 0
  return
    ",
" ||
    fn:string-join(
      (: Begin code generation block :)
      let $required-properties := (
        map:get($entity-type, "primaryKey"),
        map:get($entity-type, "required") ! json:array-values(.)
      )
      for $property-name in $property-keys
      let $is-required := $property-name = $required-properties
      return
        "    map:put($model, '" || $property-name || "', $" || service:kebob-case($property-name) || ")"
      , ",
")
  (: end code generation block :)
  }
  )

  (: if you prefer the xquery 3.1 version with the => operator....
  https://www.w3.org/TR/xquery-31/#id-arrow-operator
  let $model :=
  json:object()
  {
  if ($entity-type-name eq $entity) then
    "  =>map:with('$attachments', $attachments)"
  else ()
  }
    =>map:with('$type', '{ $entity-type-name }')
    =>map:with('$version', '{ map:get(map:get($model, "info"), "version") }')
  {
  fn:string-join(
    (: Begin code generation block :)
    let $definitions := map:get($model, "definitions")
    let $entity-type := map:get($definitions, $entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := (
      map:get($entity-type, "primaryKey"),
      json:array-values(map:get($entity-type, "required"))
    )
    for $property-name in map:keys($properties)
    let $is-required := $property-name = $required-properties
    return
      if ($is-required) then
        "  =>map:with('" || $property-name || "', $" || service:kebob-case($property-name) || ")"
      else
        "    =>es:optional('" || $property-name || "', $" || service:kebob-case($property-name) || ")"
    , "
")
  (: end code generation block :)
  }
  :)
  return $model
}};
</extract-instance>/text()
  }
declare function plugin:make-reference-object(
$type as xs:string,
$ref as xs:string)
{{
  let $o := json:object()
  let $_ := (
    map:put($o, '$type', $type),
    map:put($o, '$ref', $ref)
  )
  return
  $o
}};</module>/text()
}
};

declare function service:generate-vars($model as map:map, $entity-type-name, $mapping as map:map?, $parent-entity as xs:string)
{
  fn:string-join(
    let $definitions := map:get($model, "definitions")
    let $entity-type := map:get($definitions, $entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := (
      map:get($entity-type, "primaryKey"),
      map:get($entity-type, "required") ! json:array-values(.)
    )
    for $property-name in map:keys($properties)
    let $is-required := $property-name = $required-properties

    let $property := map:get($properties, $property-name)
    let $is-array := map:get($property, "datatype") eq "array"
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name := service:casting-function-name-sjs($property-datatype)
    let $wrap-if-array := function($str) {
      if ($is-array) then
        "json:to-array(" || $str || ")"
      else
        $str
    }
    let $ref :=
      if ($is-array) then
        let $items as map:map? := map:get($property, "items")
        return
          $items ! map:get(., "$ref")
      else
        map:get($property, "$ref")
    let $path-to-property :=
      if (service:mapping-present($mapping, $property-name) and $parent-entity eq $entity-type-name)
      then (fn:concat("fn.head(source.xpath('",service:map-value($property-name, $mapping), "'))"))
      else (fn:concat("fn.head(source.xpath('/", $property-name, "'))"))
    let $property-comment :=
      if (fn:empty($ref)) then ()
      else if (fn:contains($ref, "#/definitions")) then
        service:comment-sjs("The following property is a local reference.")
      else (
          service:comment-sjs((
            '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 :=
      fn:string-join((
        " {",
        "    '$type': '" || $ref-name || "',",
        "    '$ref': " || service:get-property($path-to-property, $ref-name),
        "  }"
      ), "
"):)
    let $value :=
      if (empty($ref)) then
        "!fn.empty(" || $path-to-property || ") ? " || (
        if($is-array) then
          $path-to-property || " : []"
        else
        $casting-function-name || "("
        || "fn.head(" ||
        $path-to-property ||
        (
          if ($property-datatype eq "iri") then
            ".toString()"
          else
            ()
        ) ||
        ")) : null"
        )
      else if (contains($ref, "#/definitions")) then
        if ($is-array) then
          fn:string-join((
            "[];",
            "if(" || $path-to-property || ") {",
            "  for(const item of Sequence.from(source.xpath('/"|| $property-name || "'))) {",
            "    // let's create and pass the node",
            "    let itemSource = new NodeBuilder();",
            "    itemSource.addNode(fn.head(item));",
            "    // this will return an instance of a " || $ref-name,
            "    " || service:camel-case($property-name) || ".push(" || service:camel-case("extractInstance-" || $ref-name) || "(itemSource.toNode()));",
            "    // or uncomment this to create an external reference to a " || $ref-name,
            "    //" || service:camel-case($property-name) || ".push(makeReferenceObject('" || $ref-name || "', itemSource.toNode()));",
            "  }",
            "}"
          ), "
  ")
        else
          fn:string-join((
            "null;",
            "if(" || $path-to-property || ") {",
            "  // let's create and pass the node",
            "  let " ||$property-name||"Source = new NodeBuilder();",
            "  " ||$property-name||"Source.addNode(fn.head(source.xpath('/"|| $property-name || "'))).toNode();",
            "  // either return an instance of a " || $ref-name,
            "  " || service:camel-case($property-name) || " = " || service:camel-case("extractInstance-" || $ref-name) || "("||$property-name||"Source);",
            "",
            "  // or a reference to a " || $ref-name,
            "  // " || service:camel-case($property-name) || " = makeReferenceObject('" || $ref-name || "', "||$property-name||"Source));",
            "}"
          ), "
  ")
      else
        if ($is-array) then
          fn:string-join((
            " null;",
            "  if (" || service:get-property($path-to-property, $ref-name) || ") {",
            "    " || service:camel-case($property-name) || " = " || service:get-property($path-to-property, $ref-name) || ".map(function(item) {",
            "      return makeReferenceObject('" || $ref-name || "', fn.head(source.xpath('/"|| $property-name || "')));",
            "    });",
            "  }"
          ), "
")
        else
          " makeReferenceObject('" || $ref-name || "', " || service:get-property($path-to-property, $ref-name) || ")"
    (:          fn:string-join((
            " {",
            "    '$type': '" || $ref-name || "',",
            "    '$ref': " || service:get-property($path-to-property, $ref-name),
            "  }"
          ), "
"):)

    return (
      $property-comment ! ("", .),
      "let " || service:camel-case($property-name) || " = " || $value || ";"
    )
    , "
  ")
(: end code generation block :)
};

declare function service:generate-sjs($entity as xs:string, $flow-type as xs:string, $model as map:map, $mapping as map:map?)
{
  let $root-name :=
    if ($flow-type eq $consts:INPUT_FLOW) then "rawContent"
    else "doc"
  return
    document {<module>'use strict'

/*
* Create Content Plugin
*
* @param id         - the identifier returned by the collector
* @param options    - an object containing options. Options are sent from Java
*
* @return - your content
*/
function createContent(id, {
  if ($flow-type eq $consts:INPUT_FLOW) then
    "rawContent, "
  else ()
}options) {{
  {
    if ($flow-type eq $consts:HARMONIZE_FLOW) then
      "let doc = cts.doc(id);"
    else ()
  }

  let source;

  // for xml we need to use xpath
  if({$root-name} && xdmp.nodeKind({$root-name}) === 'element' && {$root-name} instanceof XMLDocument) {{
    source = {$root-name}
  }}
  // for json we need to return the instance
  else if({$root-name} && {$root-name} instanceof Document) {{
    source = fn.head({$root-name}.root);
  }}
  // for everything else
  else {{
    source = {if ($flow-type eq $consts:INPUT_FLOW) then $root-name else "doc"};
  }}

  return {service:camel-case("extractInstance-" || $entity) || "(source)"};
}}
  {
for $entity-type-name in map:keys(map:get($model, "definitions"))
return <extract-instance>
/**
* Creates an object instance from some source document.
* @param source  A document or node that contains
*   data for populating a {$entity-type-name}
* @return An object with extracted data and
*   metadata about the instance.
*/
function {service:camel-case("extractInstance-" || $entity-type-name)}(source) {{
  {
    if (($entity eq $entity-type-name) and ($flow-type eq $consts:HARMONIZE_FLOW)) then
  "// the original source documents
  let attachments = source;
  // now check to see if we have XML or json, then create a node clone from the root of the instance
  if (source instanceof Element || source instanceof ObjectNode) {
    let instancePath = '/*:envelope/*:instance';
    if(source instanceof Element) {
      //make sure we grab content root only
      instancePath += '/node()[not(. instance of processing-instruction() or . instance of comment())]';
    }
    source = new NodeBuilder().addNode(fn.head(source.xpath(instancePath))).toNode();
  }
  else{
    source = new NodeBuilder().addNode(fn.head(source)).toNode();
  }
  "
    else (
  "let attachments = source;
  // now check to see if we have XML or json, then create a node clone to operate of off
  if (source instanceof Element || source instanceof ObjectNode) {
    let instancePath = '/';
    if(source instanceof Element) {
      //make sure we grab content root only
      instancePath = '/node()[not(. instance of processing-instruction() or . instance of comment())]';
    }
    source = new NodeBuilder().addNode(fn.head(source.xpath(instancePath))).toNode();
  }
  else{
    source = new NodeBuilder().addNode(fn.head(source)).toNode();
  }
  "
    )
  }
  {
    if ($entity eq $entity-type-name and fn:empty($mapping) eq fn:false() and fn:empty(map:get($mapping, "name")) eq fn:false()) then
      <txt>/* These mappings were generated using mapping: {map:get($mapping, "name")}, version: {map:get($mapping, "version")} on {fn:current-dateTime()}.*/
  </txt>/text()
    else ()
  }
  {
    service:generate-vars($model, $entity-type-name, $mapping, $entity)
  }

  // return the instance object
  return {{
  {
    if ($entity eq $entity-type-name) then
      "  '$attachments': attachments,"
    else ()
  }
    '$type': '{ $entity-type-name }',
    '$version': '{ map:get(map:get($model, "info"), "version") }'{
  let $definitions := map:get($model, "definitions")
  let $entity-type := map:get($definitions, $entity-type-name)
  let $properties := map:get($entity-type, "properties")
  let $properties-keys := map:keys($properties)
  where fn:count($properties-keys) > 0
  return
    ",
    " ||
    fn:string-join(
      (: Begin code generation block :)
      for $property-name in $properties-keys
      return
        "'" || $property-name || "': " ||  service:camel-case($property-name)
      , ",
    ")
  (: end code generation block :)
  }
  }}
}};
</extract-instance>/text()
}

function makeReferenceObject(type, ref) {{
  return {{
    '$type': type,
    '$ref': ref
  }};
}}

module.exports = {{
  createContent: createContent
}};

</module>/text()
    }
};

declare function service:map-value($key as xs:string, $mapping as map:map?) {
  let $properties := map:get($mapping, "properties")
  let $property := map:get($properties, $key)
  let $source := fn:replace(map:get($mapping, "sourceContext"), "'", '"')
  return fn:concat($source, map:get($property, "sourcedFrom"))
};

declare function service:get-mapping($mapping-name) {
  let $mapping-base-name := functx:substring-before-last($mapping-name, "-")
  let $mapping-uri := fn:concat("/mappings/", $mapping-base-name, "/", $mapping-name, ".mapping.json")
  let $mapping-doc := fn:doc($mapping-uri)
  return
    if(fn:exists($mapping-doc)) then (map:new(xdmp:from-json($mapping-doc)))
    else ()
};

declare function service:mapping-present($mapping as map:map?, $property-name as xs:string?) {
  (fn:empty($mapping) eq fn:false() and fn:empty(map:get(map:get($mapping, "properties"), $property-name)) eq fn:false())
};

declare function service:get(
  $context as map:map,
  $params  as map:map
) as document-node()*
{
  debug:dump-env(),

  perf:log('/v1/resources/validate:get', function() {
    let $entity as xs:string := map:get($params, "entity")
    let $code-format as xs:string := map:get($params, "codeFormat")
    let $flow-type as xs:string := map:get($params, "flowType")
    let $mapping-name as xs:string? := map:get($params, "mapping")
    let $mapping as map:map? := if(fn:empty($mapping-name) eq fn:false()) then (service:get-mapping($mapping-name)) else ()
    let $model as map:map? := hent:get-model($entity)
    return
      if (fn:exists($model)) then
        if((fn:empty($mapping) eq fn:false() and fn:empty($mapping-name) eq fn:false()) or (fn:empty($mapping-name) eq fn:true())) then
          if ($code-format eq "xqy") then
            service:generate-xqy($entity, $flow-type, $model, $mapping)
          else
            service:generate-sjs($entity, $flow-type, $model, $mapping)
        else
          fn:error((),"RESTAPI-SRVEXERR", (404, "Not Found", "The requested mapping: " || $mapping-name || " was not found"))
      else
        fn:error((),"RESTAPI-SRVEXERR", (404, "Not Found", "The requested entity: " || $entity || " was not found"))
  })
};

(:
  Copyright 2012-2018 MarkLogic Corporation

  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";

module namespace service = "http://marklogic.com/rest-api/extensions/scaffold-content";

import module namespace consts = "http://marklogic.com/data-hub/consts"
at "/data-hub/4/impl/consts.xqy";

import module namespace debug = "http://marklogic.com/data-hub/debug"
at "/data-hub/4/impl/debug-lib.xqy";

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

import module namespace hent = "http://marklogic.com/data-hub/hub-entities"
at "/data-hub/4/impl/hub-entities.xqy";

import module namespace perf = "http://marklogic.com/data-hub/perflog-lib"
at "/data-hub/4/impl/perflog-lib.xqy";

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

declare namespace es = "http://marklogic.com/entity-services";

declare namespace rapi = "http://marklogic.com/rest-api";

declare option xdmp:mapping "false";

declare function service:comment-xqy($comment-text as xs:string*)
{
  "(: " || fn:string-join($comment-text, "
     ") || " :)"
};

declare function service:comment-sjs($comment-text as xs:string*)
{
  "/* " || fn:string-join($comment-text, "
     ") || " */"
};

declare function service:kebob-case($str as xs:string)
{
  fn:lower-case(fn:replace(fn:replace($str, "([A-Z])", "-$1"), "^-", ""))
};

declare function service:camel-case($str as xs:string)
{
  fn:string-join(
    let $tokens := fn:tokenize($str, "[-_]")
    return
      (
        $tokens[1],
        for $x in fn:subsequence($tokens, 2)
        return
          functx:capitalize-first($x)
      ),
    "")
};

declare function service:get-property($root as xs:string, $property as xs:string)
{
  if (fn:contains($property, "-")) then
    $root || "['" || $property || "']"
  else
    $root || "." || $property
};

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

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

declare function service:generate-lets($model as map:map, $entity-type-name, $mapping as map:map?, $parent-entity as xs:string)
{
  fn:string-join(
    let $definitions := map:get($model, "definitions")
    let $entity-type := map:get($definitions, $entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := (
      map:get($entity-type, "primaryKey"),
      map:get($entity-type, "required") ! json:array-values(.)
    )
    for $property-name in map:keys($properties)
    let $is-required := $property-name = $required-properties
    let $property := map:get($properties, $property-name)
    let $is-array := map:get($property, "datatype") eq "array"
    let $property := map:get($properties, $property-name)
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name :=
      if (map:contains($property, "datatype") and map:get($property, "datatype") ne "array") then
        service:casting-function-name-xqy($property-datatype)
      else ()
    let $wrap-if-array := function($str) {
      if ($is-array) then
        "json:to-array(" || $str || ")"
      else
        $str
    }
    let $ref :=
      if ($is-array) then
        let $items as map:map? := map:get($property, "items")
        return
          $items ! map:get(., "$ref")
      else
        map:get($property, "$ref")
    let $path-to-property := fn:concat("$source-node/", $property-name)
    let $property-comment :=
      if (fn:empty($ref)) then ()
      else if (fn:contains($ref, "#/definitions")) then
        service:comment-xqy("The following property is a local reference.")
      else (
          service:comment-xqy((
            '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 :=
      fn:string-join((
        "",
        "    plugin:make-reference-object('" || $ref-name || "', (: put your value here :) '')"
      ), "
")
    let $value :=
      if (empty($ref)) then (
        if(service:mapping-present($mapping, $property-name) and $parent-entity eq $entity-type-name)
        then (fn:concat("$source",service:map-value($property-name, $mapping)))
        else ($wrap-if-array(fn:concat("$source/", $property-name))))
      else if (contains($ref, "#/definitions")) then
        let $inner-var := "$" || fn:lower-case($ref-name) || "s"
        let $inner-val :=
          if(service:mapping-present($mapping, $property-name))
          then (fn:concat("$source",service:map-value($property-name, $mapping)))
          else (fn:concat("$source/", $property-name))
        return
          fn:string-join((
            "",
            "let " || $inner-var || " :=",
            "  (: create a sequence of " || $ref-name || " instances from your data :)",
            "  for $sub-entity in ("||$inner-val||")",
            "  return",
            "    plugin:extract-instance-" || $ref-name || "($sub-entity)",
            "return",
            if ($is-required) then
              "  json:to-array(" || $inner-var || ")"
            else (
              "  if (fn:exists(" || $inner-var || ")) then",
              "    json:to-array(" || $inner-var || ")",
              "  else ()"
            )
          ), "
    ") || "
"
      else if(fn:not(empty($ref))) then
           fn:concat("$source/", $property-name)
      else $wrap-if-array($extract-reference-fn)

    return (
      $property-comment ! ("", .),

      let $name := service:kebob-case($property-name)
      let $valueWrap := fn:concat(fn:string-join(
        if ($casting-function-name) then
          $casting-function-name else ())
        , fn:concat(
          "(", $value,")"))
      return
        "let $" || $name || " := " || $valueWrap
    )
    , "
  ")
(: end code generation block :)
};

declare function service:generate-xqy($entity as xs:string, $flow-type as xs:string, $model as map:map, $mapping as map:map?)
{
  let $root-name :=
    if ($flow-type eq $consts:INPUT_FLOW) then "$raw-content"
    else "$doc"
  return
    document {
<module>xquery version "1.0-ml";

module namespace plugin = "http://marklogic.com/data-hub/plugins";

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

declare option xdmp:mapping "false";

(:~
: Create Content Plugin
:
: @param $id          - the identifier returned by the collector
: @param $options     - a map containing options. Options are sent from Java
:
: @return - your transformed content
:)
declare function plugin:create-content(
  $id as xs:string,
  {
    if ($flow-type eq $consts:INPUT_FLOW) then
      "$raw-content as node()?,
  "
    else ()
  }$options as map:map) as map:map
{{
  {
    if ($flow-type eq $consts:HARMONIZE_FLOW) then
      "let $doc := fn:doc($id)
  "
    else ()
  }let $source := {$root-name}
  return
  {
    "plugin:extract-instance-" || $entity || "($source)"
  }
}};
  {
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 plugin:extract-instance-{$entity-type-name}(
$source as node()?
) as map:map
{{
{
  if ($entity-type-name eq $entity) then
  <txt>
  (: the original source documents :)
  let $attachments := $source
  let $source      :=
    if ($source/*:envelope and $source/node() instance of element()) then
      $source/*:envelope/*:instance/node()
    else if ($source/*:envelope) then
      $source/*:envelope/*:instance
    else if ($source/instance) then
      $source/instance
    else
      $source
  </txt>/text()
  else (<txt>
  let $source :=
    if($source/node() instance of element()) then
    $source/node()
  else (
    $source
  )
  </txt>/text()
  )
}
{
  if ($entity eq $entity-type-name and fn:empty($mapping) eq fn:false() and fn:empty(map:get($mapping, "name")) eq fn:false()) then
  <txt>(: These mappings were generated using mapping: {map:get($mapping, "name")}, version: {map:get($mapping, "version")} on {fn:current-dateTime()}. :)
  </txt>/text()
  else ()
}
{
service:generate-lets($model, $entity-type-name, $mapping, $entity)
}

  (: return the in-memory instance :)
  (: using the XQuery 3.0 syntax... :)
  let $model := json:object()
  let $_ := (
  {
    if ($entity-type-name eq $entity) then
      "  map:put($model, '$attachments', $attachments),"
    else ()
  }
    map:put($model, '$type', '{ $entity-type-name }'),
    map:put($model, '$version', '{ map:get(map:get($model, "info"), "version") }'){
  let $definitions := map:get($model, "definitions")
  let $entity-type := map:get($definitions, $entity-type-name)
  let $properties := map:get($entity-type, "properties")
  let $property-keys := map:keys($properties)
  where fn:count($property-keys) > 0
  return
    ",
" ||
    fn:string-join(
      (: Begin code generation block :)
      let $required-properties := (
        map:get($entity-type, "primaryKey"),
        map:get($entity-type, "required") ! json:array-values(.)
      )
      for $property-name in $property-keys
      let $is-required := $property-name = $required-properties
      return
        "    map:put($model, '" || $property-name || "', $" || service:kebob-case($property-name) || ")"
      , ",
")
  (: end code generation block :)
  }
  )

  (: if you prefer the xquery 3.1 version with the => operator....
  https://www.w3.org/TR/xquery-31/#id-arrow-operator
  let $model :=
  json:object()
  {
  if ($entity-type-name eq $entity) then
    "  =>map:with('$attachments', $attachments)"
  else ()
  }
    =>map:with('$type', '{ $entity-type-name }')
    =>map:with('$version', '{ map:get(map:get($model, "info"), "version") }')
  {
  fn:string-join(
    (: Begin code generation block :)
    let $definitions := map:get($model, "definitions")
    let $entity-type := map:get($definitions, $entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := (
      map:get($entity-type, "primaryKey"),
      json:array-values(map:get($entity-type, "required"))
    )
    for $property-name in map:keys($properties)
    let $is-required := $property-name = $required-properties
    return
      if ($is-required) then
        "  =>map:with('" || $property-name || "', $" || service:kebob-case($property-name) || ")"
      else
        "    =>es:optional('" || $property-name || "', $" || service:kebob-case($property-name) || ")"
    , "
")
  (: end code generation block :)
  }
  :)
  return $model
}};
</extract-instance>/text()
  }
declare function plugin:make-reference-object(
$type as xs:string,
$ref as xs:string)
{{
  let $o := json:object()
  let $_ := (
    map:put($o, '$type', $type),
    map:put($o, '$ref', $ref)
  )
  return
  $o
}};</module>/text()
}
};

declare function service:generate-vars($model as map:map, $entity-type-name, $mapping as map:map?, $parent-entity as xs:string)
{
  fn:string-join(
    let $definitions := map:get($model, "definitions")
    let $entity-type := map:get($definitions, $entity-type-name)
    let $properties := map:get($entity-type, "properties")
    let $required-properties := (
      map:get($entity-type, "primaryKey"),
      map:get($entity-type, "required") ! json:array-values(.)
    )
    for $property-name in map:keys($properties)
    let $is-required := $property-name = $required-properties

    let $property := map:get($properties, $property-name)
    let $is-array := map:get($property, "datatype") eq "array"
    let $property-datatype := esi:resolve-datatype($model, $entity-type-name, $property-name)
    let $casting-function-name := service:casting-function-name-sjs($property-datatype)
    let $wrap-if-array := function($str) {
      if ($is-array) then
        "json:to-array(" || $str || ")"
      else
        $str
    }
    let $ref :=
      if ($is-array) then
        let $items as map:map? := map:get($property, "items")
        return
          $items ! map:get(., "$ref")
      else
        map:get($property, "$ref")
    let $path-to-property :=
      if (service:mapping-present($mapping, $property-name) and $parent-entity eq $entity-type-name)
      then (fn:concat("fn.head(source.xpath('",service:map-value($property-name, $mapping), "'))"))
      else (fn:concat("fn.head(source.xpath('/", $property-name, "'))"))
    let $property-comment :=
      if (fn:empty($ref)) then ()
      else if (fn:contains($ref, "#/definitions")) then
        service:comment-sjs("The following property is a local reference.")
      else (
          service:comment-sjs((
            '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 :=
      fn:string-join((
        " {",
        "    '$type': '" || $ref-name || "',",
        "    '$ref': " || service:get-property($path-to-property, $ref-name),
        "  }"
      ), "
"):)
    let $value :=
      if (empty($ref)) then
        "!fn.empty(" || $path-to-property || ") ? " || (
        if($is-array) then
          $path-to-property || " : []"
        else
        $casting-function-name || "("
        || "fn.head(" ||
        $path-to-property ||
        (
          if ($property-datatype eq "iri") then
            ".toString()"
          else
            ()
        ) ||
        ")) : null"
        )
      else if (contains($ref, "#/definitions")) then
        if ($is-array) then
          fn:string-join((
            "[];",
            "if(" || $path-to-property || ") {",
            "  for(const item of Sequence.from(source.xpath('/"|| $property-name || "'))) {",
            "    // let's create and pass the node",
            "    let itemSource = new NodeBuilder();",
            "    itemSource.addNode(fn.head(item));",
            "    // this will return an instance of a " || $ref-name,
            "    " || service:camel-case($property-name) || ".push(" || service:camel-case("extractInstance-" || $ref-name) || "(itemSource.toNode()));",
            "    // or uncomment this to create an external reference to a " || $ref-name,
            "    //" || service:camel-case($property-name) || ".push(makeReferenceObject('" || $ref-name || "', itemSource.toNode()));",
            "  }",
            "}"
          ), "
  ")
        else
          fn:string-join((
            "null;",
            "if(" || $path-to-property || ") {",
            "  // let's create and pass the node",
            "  let " ||$property-name||"Source = new NodeBuilder();",
            "  " ||$property-name||"Source.addNode(fn.head(source.xpath('/"|| $property-name || "'))).toNode();",
            "  // either return an instance of a " || $ref-name,
            "  " || service:camel-case($property-name) || " = " || service:camel-case("extractInstance-" || $ref-name) || "("||$property-name||"Source);",
            "",
            "  // or a reference to a " || $ref-name,
            "  // " || service:camel-case($property-name) || " = makeReferenceObject('" || $ref-name || "', "||$property-name||"Source));",
            "}"
          ), "
  ")
      else
        if ($is-array) then
          fn:string-join((
            " null;",
            "  if (" || service:get-property($path-to-property, $ref-name) || ") {",
            "    " || service:camel-case($property-name) || " = " || service:get-property($path-to-property, $ref-name) || ".map(function(item) {",
            "      return makeReferenceObject('" || $ref-name || "', fn.head(source.xpath('/"|| $property-name || "')));",
            "    });",
            "  }"
          ), "
")
        else
          " makeReferenceObject('" || $ref-name || "', " || service:get-property($path-to-property, $ref-name) || ")"
    (:          fn:string-join((
            " {",
            "    '$type': '" || $ref-name || "',",
            "    '$ref': " || service:get-property($path-to-property, $ref-name),
            "  }"
          ), "
"):)

    return (
      $property-comment ! ("", .),
      "let " || service:camel-case($property-name) || " = " || $value || ";"
    )
    , "
  ")
(: end code generation block :)
};

declare function service:generate-sjs($entity as xs:string, $flow-type as xs:string, $model as map:map, $mapping as map:map?)
{
  let $root-name :=
    if ($flow-type eq $consts:INPUT_FLOW) then "rawContent"
    else "doc"
  return
    document {<module>'use strict'

/*
* Create Content Plugin
*
* @param id         - the identifier returned by the collector
* @param options    - an object containing options. Options are sent from Java
*
* @return - your content
*/
function createContent(id, {
  if ($flow-type eq $consts:INPUT_FLOW) then
    "rawContent, "
  else ()
}options) {{
  {
    if ($flow-type eq $consts:HARMONIZE_FLOW) then
      "let doc = cts.doc(id);"
    else ()
  }

  let source;

  // for xml we need to use xpath
  if({$root-name} && xdmp.nodeKind({$root-name}) === 'element' && {$root-name} instanceof XMLDocument) {{
    source = {$root-name}
  }}
  // for json we need to return the instance
  else if({$root-name} && {$root-name} instanceof Document) {{
    source = fn.head({$root-name}.root);
  }}
  // for everything else
  else {{
    source = {if ($flow-type eq $consts:INPUT_FLOW) then $root-name else "doc"};
  }}

  return {service:camel-case("extractInstance-" || $entity) || "(source)"};
}}
  {
for $entity-type-name in map:keys(map:get($model, "definitions"))
return <extract-instance>
/**
* Creates an object instance from some source document.
* @param source  A document or node that contains
*   data for populating a {$entity-type-name}
* @return An object with extracted data and
*   metadata about the instance.
*/
function {service:camel-case("extractInstance-" || $entity-type-name)}(source) {{
  {
    if (($entity eq $entity-type-name) and ($flow-type eq $consts:HARMONIZE_FLOW)) then
  "// the original source documents
  let attachments = source;
  // now check to see if we have XML or json, then create a node clone from the root of the instance
  if (source instanceof Element || source instanceof ObjectNode) {
    let instancePath = '/*:envelope/*:instance';
    if(source instanceof Element) {
      //make sure we grab content root only
      instancePath += '/node()[not(. instance of processing-instruction() or . instance of comment())]';
    }
    source = new NodeBuilder().addNode(fn.head(source.xpath(instancePath))).toNode();
  }
  else{
    source = new NodeBuilder().addNode(fn.head(source)).toNode();
  }
  "
    else (
  "// now check to see if we have XML or json, then create a node clone to operate of off
  if (source instanceof Element || source instanceof ObjectNode) {
    let instancePath = '/';
    if(source instanceof Element) {
      //make sure we grab content root only
      instancePath = '/node()[not(. instance of processing-instruction() or . instance of comment())]';
    }
    source = new NodeBuilder().addNode(fn.head(source.xpath(instancePath))).toNode();
  }
  else{
    source = new NodeBuilder().addNode(fn.head(source)).toNode();
  }
  "
    )
  }
  {
    if ($entity eq $entity-type-name and fn:empty($mapping) eq fn:false() and fn:empty(map:get($mapping, "name")) eq fn:false()) then
      <txt>/* These mappings were generated using mapping: {map:get($mapping, "name")}, version: {map:get($mapping, "version")} on {fn:current-dateTime()}.*/
  </txt>/text()
    else ()
  }
  {
    service:generate-vars($model, $entity-type-name, $mapping, $entity)
  }

  // return the instance object
  return {{
  {
    if ($entity eq $entity-type-name) then
      "  '$attachments': attachments,"
    else ()
  }
    '$type': '{ $entity-type-name }',
    '$version': '{ map:get(map:get($model, "info"), "version") }'{
  let $definitions := map:get($model, "definitions")
  let $entity-type := map:get($definitions, $entity-type-name)
  let $properties := map:get($entity-type, "properties")
  let $properties-keys := map:keys($properties)
  where fn:count($properties-keys) > 0
  return
    ",
    " ||
    fn:string-join(
      (: Begin code generation block :)
      for $property-name in $properties-keys
      return
        "'" || $property-name || "': " ||  service:camel-case($property-name)
      , ",
    ")
  (: end code generation block :)
  }
  }}
}};
</extract-instance>/text()
}

function makeReferenceObject(type, ref) {{
  return {{
    '$type': type,
    '$ref': ref
  }};
}}

module.exports = {{
  createContent: createContent
}};

</module>/text()
    }
};

declare function service:map-value($key as xs:string, $mapping as map:map?) {
  let $properties := map:get($mapping, "properties")
  let $property := map:get($properties, $key)
  let $source := fn:replace(map:get($mapping, "sourceContext"), "'", '"')
  return fn:concat($source, map:get($property, "sourcedFrom"))
};

declare function service:get-mapping($mapping-name) {
  let $mapping-base-name := functx:substring-before-last($mapping-name, "-")
  let $mapping-uri := fn:concat("/mappings/", $mapping-base-name, "/", $mapping-name, ".mapping.json")
  let $mapping-doc := fn:doc($mapping-uri)
  return
    if(fn:exists($mapping-doc)) then (map:new(xdmp:from-json($mapping-doc)))
    else ()
};

declare function service:mapping-present($mapping as map:map?, $property-name as xs:string?) {
  (fn:empty($mapping) eq fn:false() and fn:empty(map:get(map:get($mapping, "properties"), $property-name)) eq fn:false())
};

declare function service:get(
  $context as map:map,
  $params  as map:map
) as document-node()*
{
  debug:dump-env(),

  perf:log('/v1/resources/validate:get', function() {
    let $entity as xs:string := map:get($params, "entity")
    let $code-format as xs:string := map:get($params, "codeFormat")
    let $flow-type as xs:string := map:get($params, "flowType")
    let $mapping-name as xs:string? := map:get($params, "mapping")
    let $mapping as map:map? := if(fn:empty($mapping-name) eq fn:false()) then (service:get-mapping($mapping-name)) else ()
    let $model as map:map? := hent:get-model($entity)
    return
      if (fn:exists($model)) then
        if((fn:empty($mapping) eq fn:false() and fn:empty($mapping-name) eq fn:false()) or (fn:empty($mapping-name) eq fn:true())) then
          if ($code-format eq "xqy") then
            service:generate-xqy($entity, $flow-type, $model, $mapping)
          else
            service:generate-sjs($entity, $flow-type, $model, $mapping)
        else
          fn:error((),"RESTAPI-SRVEXERR", (404, "Not Found", "The requested mapping: " || $mapping-name || " was not found"))
      else
        fn:error((),"RESTAPI-SRVEXERR", (404, "Not Found", "The requested entity: " || $entity || " was not found"))
  })
};


[FIX] DBA: Jobs&Users, sessions. Closes #1321

(:~
 : Users page.
 :
 : @author Christian Grün, BaseX Team, 2014-16
 :)
module namespace _ = 'dba/jobs-users';

import module namespace Sessions = 'http://basex.org/modules/sessions';
import module namespace Session = 'http://basex.org/modules/session';
import module namespace cons = 'dba/cons' at '../modules/cons.xqm';
import module namespace html = 'dba/html' at '../modules/html.xqm';
import module namespace tmpl = 'dba/tmpl' at '../modules/tmpl.xqm';
import module namespace util = 'dba/util' at '../modules/util.xqm';

(:~ Top category :)
declare variable $_:CAT := 'jobs-users';

(:~
 : Jobs and users page.
 : @param  $sort   table sort key
 : @param  $error  error message
 : @param  $info   info message
 : @return page
 :)
declare
  %rest:GET
  %rest:path("/dba/jobs-users")
  %rest:query-param("sort", "{$sort}", "")
  %rest:query-param("error", "{$error}")
  %rest:query-param("info",  "{$info}")
  %output:method("html")
function _:jobs-users(
  $sort   as xs:string,
  $error  as xs:string?,
  $info   as xs:string?
) as element() {
  cons:check(),

  (: request data in a single step :)
  let $data := try {
    util:eval('element result {
      element users { user:list-details() },
      element sessions { admin:sessions() },
      element jobs { jobs:list-details() },
      element current-job { jobs:current() }
    }')
  } catch * {
    element error { $cons:DATA-ERROR || ': ' || $err:description }
  }
  let $error := ($data/self::error/string(), $error)[1]

  return tmpl:wrap(map { 'top': $_:CAT, 'info': $info, 'error': $error },

        <form action="{ $_:CAT }" method="post" class="update">

Jobs

        {
          let $entries :=
            let $curr := $data/current-job
            for $job in $data/jobs/job
            let $id := $job/@id
            let $ms := xs:dayTimeDuration($job/@duration) div xs:dayTimeDuration('PT0.001S')
            let $you := if($id = $curr) then '✓' else '–'
            order by $ms descending
            return <e id='{ $id }' type='{ $job/@type }' state='{ $job/@state }'
              user='{ $job/@user }' sec='{ $ms div 1000 }' start='{ $job/@start}'
              end='{ $job/@end }' you='{ $you }'/>

          let $headers := (
            <id>{ html:label($entries, ('ID', 'IDs')) }</id>,
            <type>Type</type>,
            <state>State</state>,
            <user>User</user>,
            <sec type='number' order='desc'>Seconds</sec>,
            <start type='dateTime' order='desc'>Start</start>,
            <end type='dateTime' order='desc'>End</end>,
            <you>You</you>
          )
          let $buttons := (
            html:button('stop-job', 'Stop', true())
          )
          let $link := function($value) { 'job' }
          return html:table($entries, $headers, $buttons, map {}, $sort, $link)
        }
        </form>
        <form action="{ $_:CAT }" method="post" class="update">

Server Sessions

        {
          let $entries :=
            for $id in Sessions:ids()
            let $access := Sessions:accessed($id)
            let $you := if(Session:id() = $id) then '✓' else '–'
            for $name in Sessions:names($id)
            for $value in try {
              let $attr := Sessions:get($id, $name)
              return if($attr instance of element()) then (
                $attr/name
              ) else if($attr instance of xs:string) then (
                $attr
              ) else (
                '...'
              )
            } catch bxerr:BXSE0002 {
              (: ignore non-XQuery session values :)
            }
            return <e id='{ $id || '|' || $name }' name='{ $name }' value='{ $value }'
             access='{ $access }' you='{ $you }'/>
          let $headers := (
            <id type='id'>{  html:label($entries, ('ID', 'IDs')) }</id>,
            <name>Name</name>,
            <value>Value</value>,
            <access type='dateTime' order='desc'>Last Access</access>,
            <you>You</you>
          )
          let $buttons := (
            html:button('kill-session', 'Kill', true())
          )
          return html:table($entries, $headers, $buttons)
        }
        </form>

Database Clients

        {
          let $entries := $data/sessions/session/<e addr='{ @address }' user='{ @user }'/>
          let $headers := (
            <addr>{ html:label($entries, ('Session', 'Sessions')) }</addr>,
            <user>Address</user>
          )
          return html:table($entries, $headers, ())
        }

        <form action="{ $_:CAT }" method="post" class="update">

Users

        {
          let $entries :=
            for $entry in $data/users/user
            let $you := if($cons:SESSION/name = $entry/@name) then '✓' else '–'
            return <e name='{ $entry/@name }' perm='{ $entry/@permission }' you='{ $you }'/>
          let $headers := (
            <name>{ html:label($entries, ('User', 'Users')) }</name>,
            <perm>Permission</perm>,
            <you>You</you>
          )
          let $buttons := (
            html:button('create-user', 'Create…'),
            html:button('drop-user', 'Drop', true())
          )
          let $link := function($value) { 'user' }
          return html:table($entries, $headers, $buttons)
        }
        </form>

  )
};

(:~
 : Redirects to the specified action.
 : @param  $action  action to perform
 : @param  $names   names of users
 : @param  $ids     ids
 :)
declare
  %rest:POST
  %rest:path("/dba/jobs-users")
  %rest:query-param("action", "{$action}")
  %rest:query-param("name",   "{$names}")
  %rest:query-param("id",     "{$ids}")
  %output:method("html")
function _:action(
  $action  as xs:string,
  $names   as xs:string*,
  $ids     as xs:string*
) {
  web:redirect($action,
    if($action = 'create-user') then map { }
    else if($action = 'kill-session') then map { 'id': $ids }
    else if($action = 'stop-job') then map { 'id': $ids }
    else map { 'name': $names, 'redirect': $_:CAT }
  )
};

(:~
 : Users page.
 :
 : @author Christian Grün, BaseX Team, 2014-16
 :)
module namespace _ = 'dba/jobs-users';

import module namespace Sessions = 'http://basex.org/modules/sessions';
import module namespace Session = 'http://basex.org/modules/session';
import module namespace cons = 'dba/cons' at '../modules/cons.xqm';
import module namespace html = 'dba/html' at '../modules/html.xqm';
import module namespace tmpl = 'dba/tmpl' at '../modules/tmpl.xqm';
import module namespace util = 'dba/util' at '../modules/util.xqm';

(:~ Top category :)
declare variable $_:CAT := 'jobs-users';

(:~
 : Jobs and users page.
 : @param  $sort   table sort key
 : @param  $error  error message
 : @param  $info   info message
 : @return page
 :)
declare
  %rest:GET
  %rest:path("/dba/jobs-users")
  %rest:query-param("sort", "{$sort}", "")
  %rest:query-param("error", "{$error}")
  %rest:query-param("info",  "{$info}")
  %output:method("html")
function _:jobs-users(
  $sort   as xs:string,
  $error  as xs:string?,
  $info   as xs:string?
) as element() {
  cons:check(),

  (: request data in a single step :)
  let $data := try {
    util:eval('element result {
      element users { user:list-details() },
      element sessions { admin:sessions() },
      element jobs { jobs:list-details() },
      element current-job { jobs:current() }
    }')
  } catch * {
    element error { $cons:DATA-ERROR || ': ' || $err:description }
  }
  let $error := ($data/self::error/string(), $error)[1]

  return tmpl:wrap(map { 'top': $_:CAT, 'info': $info, 'error': $error },

        <form action="{ $_:CAT }" method="post" class="update">

Jobs

        {
          let $entries :=
            let $curr := $data/current-job
            for $job in $data/jobs/job
            let $id := $job/@id
            let $ms := xs:dayTimeDuration($job/@duration) div xs:dayTimeDuration('PT0.001S')
            let $you := if($id = $curr) then '✓' else '–'
            order by $ms descending
            return <e id='{ $id }' type='{ $job/@type }' state='{ $job/@state }'
              user='{ $job/@user }' sec='{ $ms div 1000 }' start='{ $job/@start}'
              end='{ $job/@end }' you='{ $you }'/>

          let $headers := (
            <id>{ html:label($entries, ('ID', 'IDs')) }</id>,
            <type>Type</type>,
            <state>State</state>,
            <user>User</user>,
            <sec type='number' order='desc'>Seconds</sec>,
            <start type='dateTime' order='desc'>Start</start>,
            <end type='dateTime' order='desc'>End</end>,
            <you>You</you>
          )
          let $buttons := (
            html:button('stop-job', 'Stop', true())
          )
          let $link := function($value) { 'job' }
          return html:table($entries, $headers, $buttons, map {}, $sort, $link)
        }
        </form>
        <form action="{ $_:CAT }" method="post" class="update">

Server Sessions

        {
          let $entries :=
            for $id in Sessions:ids()
            for $name in Sessions:names($id)
            for $session in Sessions:get($id, $name)
            let $access := Sessions:accessed($id)
            let $you := if(Session:id() = $id) then '✓' else '–'
            let $value :=
              if ($session instance of element()) then $session/name
              else if($session instance of xs:string) then $session else ()
            return <e id='{ $id || '|' || $name }' name='{ $name }' value='{ $value }'
             access='{ $access }' you='{ $you }'/>
          let $headers := (
            <id type='id'>{  html:label($entries, ('ID', 'IDs')) }</id>,
            <name>Name</name>,
            <value>Value</value>,
            <access type='dateTime' order='desc'>Last Access</access>,
            <you>You</you>
          )
          let $buttons := (
            html:button('kill-session', 'Kill', true())
          )
          return html:table($entries, $headers, $buttons)
        }
        </form>

Database Clients

        {
          let $entries := $data/sessions/session/<e addr='{ @address }' user='{ @user }'/>
          let $headers := (
            <addr>{ html:label($entries, ('Session', 'Sessions')) }</addr>,
            <user>Address</user>
          )
          return html:table($entries, $headers, ())
        }

        <form action="{ $_:CAT }" method="post" class="update">

Users

        {
          let $entries :=
            for $entry in $data/users/user
            let $you := if($cons:SESSION/name = $entry/@name) then '✓' else '–'
            return <e name='{ $entry/@name }' perm='{ $entry/@permission }' you='{ $you }'/>
          let $headers := (
            <name>{ html:label($entries, ('User', 'Users')) }</name>,
            <perm>Permission</perm>,
            <you>You</you>
          )
          let $buttons := (
            html:button('create-user', 'Create…'),
            html:button('drop-user', 'Drop', true())
          )
          let $link := function($value) { 'user' }
          return html:table($entries, $headers, $buttons)
        }
        </form>

  )
};

(:~
 : Redirects to the specified action.
 : @param  $action  action to perform
 : @param  $names   names of users
 : @param  $ids     ids
 :)
declare
  %rest:POST
  %rest:path("/dba/jobs-users")
  %rest:query-param("action", "{$action}")
  %rest:query-param("name",   "{$names}")
  %rest:query-param("id",     "{$ids}")
  %output:method("html")
function _:action(
  $action  as xs:string,
  $names   as xs:string*,
  $ids     as xs:string*
) {
  web:redirect($action,
    if($action = 'create-user') then map { }
    else if($action = 'kill-session') then map { 'id': $ids }
    else if($action = 'stop-job') then map { 'id': $ids }
    else map { 'name': $names, 'redirect': $_:CAT }
  )
};


Changed to target separate directory full of notes

module namespace notes="http://cefn.com/readinglog/notes";

import module namespace template="http://cefn.com/readinglog/template" at "/home/cefn/Documents/highwire/participatory firmware design/readinglog/scripts/server/xquery/lib/template.xq";

declare namespace ann = "http://www.zorba-xquery.com/annotations";

declare default element namespace "http://www.w3.org/1999/xhtml";

declare variable $notes:headings := ('Concepts','Methodology','Field of Study','Quotations','References');

declare %ann:nondeterministic function notes:allnotes(){
	template:loadhtml("resources/notes/")
};

declare function notes:followingheader($nodes,$title){
	$nodes//*[preceding-sibling::h2[1][text()=$title]]
};

module namespace notes="http://cefn.com/readinglog/notes";

import module namespace template="http://cefn.com/readinglog/template" at "/home/cefn/Documents/highwire/participatory firmware design/readinglog/scripts/server/xquery/lib/template.xq";

declare namespace ann = "http://www.zorba-xquery.com/annotations";

declare default element namespace "http://www.w3.org/1999/xhtml";

declare variable $notes:headings := ('Concepts','Methodology','Field of Study','Quotations','References');

declare %ann:nondeterministic function notes:allnotes(){
	template:loadhtml("../../notes/")
};

declare function notes:followingheader($nodes,$title){
	$nodes//*[preceding-sibling::h2[1][text()=$title]]
};


Forgot to commit test/parser

Forgot to commit test/parser

try {
  <a/>
}
catch err:XPST0051 {
  <b/>
}
catch err:XPST0080 {
  "text"
}
catch * ($error) {
  "Error: " ,$error
}

try {
  <a/>
}
catch (err:XPST0051) {
  <b/>
}
catch (err:XPST0080) {
  "text"
}
catch (*, $error) {
  "Error: " ,$error
}


fixed up submission of updating CSRs

fixed up submission of updating CSRs

module namespace page = 'http://basex.org/modules/web-page';

import module namespace csr_proc = "https://github.com/openhie/openinfoman/csr_proc";
import module namespace csd_dm = "https://github.com/openhie/openinfoman/csd_dm";
import module namespace csd_webconf =  "https://github.com/openhie/openinfoman/csd_webconf";

declare   namespace   csd = "urn:ihe:iti:csd:2013";
declare default element  namespace   "urn:ihe:iti:csd:2013";

declare variable $page:csd_docs := csd_dm:registered_documents($csd_webconf:db);

declare
  %rest:path("/CSD/csr/{$name}/careServicesRequest")
  %rest:POST("{$careServicesRequest}")
  function page:csr($name,$careServicesRequest)
{
if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
 csr_proc:process_CSR($csd_webconf:db,$careServicesRequest/careServicesRequest,$name,$csd_webconf:baseurl)
else
  (:need appropriate error handling:)
  ()

};

declare
  %rest:path("/CSD/csr/{$name}/careServicesRequest/{$search}")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST("{$requestParams}")
  function page:csr2($name,$requestParams,$search)
{
let $careServicesRequest :=
  <csd:careServicesRequest>
    <csd:function urn="{$search}">{$requestParams}</csd:function>
  </csd:careServicesRequest>
return
  if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
    csr_proc:process_CSR($csd_webconf:db,$careServicesRequest,$name,$csd_webconf:baseurl)
  else
    (:need appropriate error handling:)
  ()

};

declare updating
  %rest:path("/CSD/csr/{$name}/careServicesRequest/update")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST("{$careServicesRequest}")
  function page:csr_updating($name,$careServicesRequest)
{
  if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
    csr_proc:process_updating_CSR($csd_webconf:db,$careServicesRequest/csd:careServicesRequest,$name,$csd_webconf:baseurl)
  else
    (:need appropriate error handling:)
    ()
};

declare updating
  %rest:path("/CSD/csr/{$name}/careServicesRequest/update/{$search}")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST("{$requestParams}")
  function page:csr_updating($name,$requestParams,$search)
{
let $careServicesRequest :=
  <csd:careServicesRequest>
    <csd:function urn="{$search}">{$requestParams}</csd:function>
  </csd:careServicesRequest>
return
if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
 csr_proc:process_updating_CSR($csd_webconf:db,$careServicesRequest,$name,$csd_webconf:baseurl)
else
  (:need appropriate error handling:)
  ()

};

declare
  %rest:path("/CSD/csr/{$name}/adhoc")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST
  %rest:form-param("adhoc","{$adhoc}")
  %rest:form-param("content", "{$content}","application/xml")
function page:adhoc($name,$adhoc,$content) {
if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
let  $adhoc_doc := csr_proc:create_adhoc_doc(string($adhoc),$content)
  return  csr_proc:process_CSR($csd_webconf:db, $adhoc_doc,$name,$csd_webconf:baseurl)
else
  (:need appropriate error handling:)
  ()

};

declare
  %rest:path("/CSD/csr")
  %rest:GET
  %output:method("xhtml")
  function page:csr_list()
{
let $response := page:endpoints()
return csd_webconf:wrapper($response)
};

declare function page:endpoints() {

Care Services Request - Endpoints

      {
	for $name in $page:csd_docs
	return

	  Submit Care Services Request for {$name} at:

{$csd_webconf:baseurl}CSD/csr/{$name}/careServicesRequest

	  Submit ad-hoc query:
	  <form method='post' action="/CSD/csr/{$name}/adhoc"  enctype="multipart/form-data">
	    Ad-Hoc Query<textarea  rows="10" cols="80" name="adhoc" >{$page:sample}</textarea>

	    Content Type<input    cols="80" name="content" value="text/html"/>

	    <input type="submit" value="submit"/>
	  </form>

      }

};

declare variable $page:sample :=
"declare namespace csd = 'urn:ihe:iti:csd:2013';

You have {count(/csd:CSD/csd:providerDirectory/*)} providers.

You have {count(/csd:CSD/csd:facilityDirectory/*)} facilities.

You have {count(/csd:CSD/csd:organizationDirectory/*)} organizations.

You have {count(/csd:CSD/csd:serviceDirectory/*)} services.

";

module namespace page = 'http://basex.org/modules/web-page';

import module namespace csr_proc = "https://github.com/openhie/openinfoman/csr_proc";
import module namespace csd_dm = "https://github.com/openhie/openinfoman/csd_dm";
import module namespace csd_webconf =  "https://github.com/openhie/openinfoman/csd_webconf";

declare   namespace   csd = "urn:ihe:iti:csd:2013";
declare default element  namespace   "urn:ihe:iti:csd:2013";

declare variable $page:csd_docs := csd_dm:registered_documents($csd_webconf:db);

declare
  %rest:path("/CSD/csr/{$name}/careServicesRequest")
  %rest:POST("{$careServicesRequest}")
  function page:csr($name,$careServicesRequest)
{
if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
 csr_proc:process_CSR($csd_webconf:db,$careServicesRequest/careServicesRequest,$name,$csd_webconf:baseurl)
else
  (:need appropriate error handling:)
  ()

};

declare
  %rest:path("/CSD/csr/{$name}/careServicesRequest/{$search}")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST("{$requestParams}")
  function page:csr2($name,$requestParams,$search)
{
let $careServicesRequest :=
  <csd:careServicesRequest>
    <csd:function urn="{$search}">{$requestParams}</csd:function>
  </csd:careServicesRequest>
return
  if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
    csr_proc:process_CSR($csd_webconf:db,$careServicesRequest,$name,$csd_webconf:baseurl)
  else
    (:need appropriate error handling:)
  ()

};

declare updating
  %rest:path("/CSD/csr/{$name}/careServicesRequest/update")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST("{$careServicesRequest}")
  function page:csr_updating($name,$careServicesRequest)
{
  if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
    csr_proc:process_updating_CSR($csd_webconf:db,$careServicesRequest/careServicesRequest,$name,$csd_webconf:baseurl)
  else
    (:need appropriate error handling:)
    ()
};

declare updating
  %rest:path("/CSD/csr/{$name}/careServicesRequest/update/{$search}")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST("{$requestParams}")
  function page:csr_updating($name,$requestParams,$search)
{
let $careServicesRequest :=
  <csd:careServicesRequest>
    <csd:function urn="{$search}">{$requestParams}</csd:function>
  </csd:careServicesRequest>
return
if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
 csr_proc:process_updating_CSR($csd_webconf:db,$careServicesRequest/careServicesRequest,$name,$csd_webconf:baseurl)
else
  (:need appropriate error handling:)
  ()

};

declare
  %rest:path("/CSD/csr/{$name}/adhoc")
  %rest:consumes("application/xml", "text/xml", "multipart/form-data")
  %rest:POST
  %rest:form-param("adhoc","{$adhoc}")
  %rest:form-param("content", "{$content}","application/xml")
function page:adhoc($name,$adhoc,$content) {
if (csd_dm:document_source_exists($csd_webconf:db,$name)) then
let  $adhoc_doc := csr_proc:create_adhoc_doc(string($adhoc),$content)
  return  csr_proc:process_CSR($csd_webconf:db, $adhoc_doc,$name,$csd_webconf:baseurl)
else
  (:need appropriate error handling:)
  ()

};

declare
  %rest:path("/CSD/csr")
  %rest:GET
  %output:method("xhtml")
  function page:csr_list()
{
let $response := page:endpoints()
return csd_webconf:wrapper($response)
};

declare function page:endpoints() {

Care Services Request - Endpoints

      {
	for $name in $page:csd_docs
	return

	  Submit Care Services Request for {$name} at:

{$csd_webconf:baseurl}CSD/csr/{$name}/careServicesRequest

	  Submit ad-hoc query:
	  <form method='post' action="/CSD/csr/{$name}/adhoc"  enctype="multipart/form-data">
	    Ad-Hoc Query<textarea  rows="10" cols="80" name="adhoc" >{$page:sample}</textarea>

	    Content Type<input    cols="80" name="content" value="text/html"/>

	    <input type="submit" value="submit"/>
	  </form>

      }

};

declare variable $page:sample :=
"declare namespace csd = 'urn:ihe:iti:csd:2013';

You have {count(/csd:CSD/csd:providerDirectory/*)} providers.

You have {count(/csd:CSD/csd:facilityDirectory/*)} facilities.

You have {count(/csd:CSD/csd:organizationDirectory/*)} organizations.

You have {count(/csd:CSD/csd:serviceDirectory/*)} services.

";


add note about empty key attributes to the documentation of insert, get, and remove

xquery version "3.0";
(:
 : Copyright 2006-2012 The FLWOR Foundation.
 :
 : 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.
:)

(:~
 :
This module defines a set of functions for working with maps. A map
 : is identified by a QName and can be created using the map:create or
 : map:create-transient functions and deleted using the map:delete function,
 : respectively.

 :
 :
The lifetime of a transient map is limited by the execution of the
 : current XQuery program. A non-transient (or persistent) map lives until
 : it is explicitly deleted. Accordingly, it's also available
 : to other XQuery programs.

 :
 :
The key of a particular entry in the map can consist of a tuple of
 : atomic values (called attributes). The actual type of each attribute
 : is determined when the map is created. The value of each attribute can
 : be an arbitrary sequence of items.

 :
 : @see Data Lifecycle
 : @see http://www.zorba-xquery.com/errors
 :
 : @author Matthias Brantner
 :
 : @project store/data structures
 :)
module namespace map = "http://www.zorba-xquery.com/modules/store/data-structures/unordered-map";

declare namespace zerr = "http://www.zorba-xquery.com/errors";
declare namespace err = "http://www.w3.org/2005/xqt-errors";
declare namespace an = "http://www.zorba-xquery.com/annotations";

declare namespace ver = "http://www.zorba-xquery.com/options/versioning";
declare option ver:module-version "2.0";

(:~
 : Create a map with a given name and a set of type identifiers for each key
 : attribute. Note that the function is variadic and might take an arbitrary
 : number of type identifiers for the key attributes. Also note that the
 : function is sequential and immediately creates the map in the store.
 :
 : @param $name the name of the map
 : @param $key-type an arbitrary number of types, one
 :        for each key attribute.
 :
 : @return the function is sequential and immediately creates
 :         the corresponding map but returns the empty-sequence.
 :
 : @error err:XPTY0004 if any of the attribute types is not a subtype of
 :        xs:anyAtomicType.
 : @error zerr:ZSTR0001 if a map with the given name already exists.
 :)
declare %an:variadic %an:sequential function map:create(
  $name as xs:QName,
  $key-type as xs:QName) as empty-sequence() external;

(:~
 : Create a transient map with a given name and a set of type identifiers
 : for each key attribute. Note that the function is variadic and might
 : take an arbitrary number of type identifiers for the key attributes.
 :
 : @param $name the name of the map
 : @param $key-type an arbitrary number of types, one
 :        for each key attribute.
 :
 : @return the function is sequential and immediately creates
 :         the corresponding map but returns the empty-sequence.
 :
 : @error err:XPTY0004 if any of the attribute types is not a subtype of
 :        xs:anyAtomicType.
 : @error zerr:ZSTR0001 if a map with the given name already exists.
 :)
declare %an:variadic %an:sequential function map:create-transient(
  $name as xs:QName,
  $key-type as xs:QName) as empty-sequence() external;

(:~
 : Destroys the map with the given name.
 :
 : @param $name the name of the map to delete
 :
 : @return the function is sequential and immediately deletes
 :         the map but returns the empty-sequence.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :)
declare %an:sequential function map:delete(
  $name as xs:QName) as empty-sequence() external;

(:~
 : Inserts a new entry into the map with the given name. Note that the
 : function is variadic and might take an arbitrary number of key attributes.
 : If an entry with the given key already exists in the map, the value
 : sequences of the existing entry and the sequence passed using $value
 : argument are concatenated.
 :
 : Note that it is possible to insert entries with empty key attributes.
 : However as the getting the entries is based on the "eq" comparison and
 : as "eq" with an empty sequence always return false, it is not possible
 : to retrieve these entries.
 :
 : @param $name the name of the map
 : @param $value the value of the entry to insert
 : @param $key an arbitrary number of key attributes.
 :
 : @return the function is sequential and immediately inserts
 :         the entry into the map but returns the
 :         empty-sequence.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 : @error zerr:ZDDY0025 if the given number of key attributes does not match
 :        the number of key attributes specified when creating
 :        the map (see the map:create function).
 : @error zerr:ZXQD0005 if any of the given key attributes can not
 :        be cast (or is not a subtype) of the corresponding key attribute
 :        specified when creating the map.
 :
 : @see map:create
 :
 :)
declare %an:variadic %an:sequential function map:insert(
  $name as xs:QName,
  $value as item()*,
  $key as xs:anyAtomicType?) as empty-sequence() external;

(:~
 : Returns the value of the entry with the given key from the map.
 :
 : Note that it is possible to insert entries with empty key attributes.
 : However as the getting the entries is based on the "eq" comparison and
 : as "eq" with an empty sequence always return false, it is not possible
 : to retrieve these entries.
 :
 : @param $name the name of the map
 : @param an arbitrary number of search key attributes.
 :
 : @return the value of the entry in the map identified
 :         by the given key. The empty-sequence will be returned
 :         if no entry with the given key is contained in the map.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 : @error zerr:ZDDY0025 if the given number of key attributes does not match
 :        the number of key attributes specified when creating
 :        the map (see the map:create function).
 : @error zerr:ZXQD0005 if any of the given key attributes can not
 :        be cast (or is not a subtype) of the corresponding key attribute
 :        specified when creating the map.
 :
 : @see map:create
 :)
declare %an:variadic function map:get(
  $name as xs:QName,
  $key as xs:anyAtomicType?) as item()* external;

(:~
 : Removes an entry identified by the given key from the map.
 :
 : Note that it is possible to insert entries with empty key attributes.
 : However as the removing the entries is based on the "eq" comparison and
 : as "eq" with an empty sequence always return false, it is not possible
 : to remove these entries.
 :
 : @param $name the name of the map
 : @param an arbitrary number of search key attributes.
 :
 : @return the function is sequential and immediately removes
 :         the entry into the map but returns the
 :         empty-sequence.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 : @error zerr:ZDDY0025 if the given number of key attributes does not match
 :        the number of key attributes specified when creating
 :        the map (see the map:create function).
 : @error zerr:ZXQD0005 if any of the given key attributes can not
 :        be cast (or is not a subtype) of the corresponding key attribute
 :        specified when creating the map.
 :
 : @see map:create
 :)
declare %an:variadic %an:sequential function map:remove(
  $name as xs:QName,
  $key as xs:anyAtomicType?) as empty-sequence() external;

(:~
 : Returns the keys of all entries of a map. The keys
 : are returned as sequence of nodes of the form:
 :
 :

 : <key xmlns="http://www.zorba-xquery.com/modules/store/data-structures/unordered-map">
 :   <attribute value="key1_value"/>
 :   <attribute value="key2_value"/>
 :   <attribute value="key3_value"/>
 : </key>
 :

 :
 : The following condition always holds:
 : <tt>map:size($name) eq fn:count(map:keys($name))</tt>
 :
 : @param $name the name of the map
 :
 : @return all keys in the map.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :
 :)
declare function map:keys(
  $name as xs:QName) as node()* external;

(:~
 : The number of entries in a map.
 :
 : The following condition always holds:
 : <tt>map:size($name) eq fn:count(map:keys($name))</tt>
 :
 : @param $name the name of the map
 :
 : @return the number of entries in the map.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :)
declare function map:size($name as xs:QName) as xs:integer external;

(:~
 : The function returns a sequence of QNames of the maps that are
 : available. The sequence will be empty if there are no maps.
 :
 : @return A sequence of QNames, one for each available map, or an emtpy sequence.
 :
 :)
declare function map:available-maps() as xs:QName* external;

(:~
 : The function returns true if the map identified by the given QName
 : is transient, false otherwise.
 :
 : @param $name the name of the map
 :
 : @return true if the map is transient, false otherwise.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :)
declare function map:is-transient($name as xs:QName) as xs:boolean external;

xquery version "3.0";
(:
 : Copyright 2006-2012 The FLWOR Foundation.
 :
 : 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.
:)

(:~
 :
This module defines a set of functions for working with maps. A map
 : is identified by a QName and can be created using the map:create or
 : map:create-transient functions and deleted using the map:delete function,
 : respectively.

 :
 :
The lifetime of a transient map is limited by the execution of the
 : current XQuery program. A non-transient (or persistent) map lives until
 : it is explicitly deleted. Accordingly, it's also available
 : to other XQuery programs.

 :
 :
The key of a particular entry in the map can consist of a tuple of
 : atomic values (called attributes). The actual type of each attribute
 : is determined when the map is created. The value of each attribute can
 : be an arbitrary sequence of items.

 :
 : @see Data Lifecycle
 : @see http://www.zorba-xquery.com/errors
 :
 : @author Matthias Brantner
 :
 : @project store/data structures
 :)
module namespace map = "http://www.zorba-xquery.com/modules/store/data-structures/unordered-map";

declare namespace zerr = "http://www.zorba-xquery.com/errors";
declare namespace err = "http://www.w3.org/2005/xqt-errors";
declare namespace an = "http://www.zorba-xquery.com/annotations";

declare namespace ver = "http://www.zorba-xquery.com/options/versioning";
declare option ver:module-version "2.0";

(:~
 : Create a map with a given name and a set of type identifiers for each key
 : attribute. Note that the function is variadic and might take an arbitrary
 : number of type identifiers for the key attributes. Also note that the
 : function is sequential and immediately creates the map in the store.
 :
 : @param $name the name of the map
 : @param $key-type an arbitrary number of types, one
 :        for each key attribute.
 :
 : @return the function is sequential and immediately creates
 :         the corresponding map but returns the empty-sequence.
 :
 : @error err:XPTY0004 if any of the attribute types is not a subtype of
 :        xs:anyAtomicType.
 : @error zerr:ZSTR0001 if a map with the given name already exists.
 :)
declare %an:variadic %an:sequential function map:create(
  $name as xs:QName,
  $key-type as xs:QName) as empty-sequence() external;

(:~
 : Create a transient map with a given name and a set of type identifiers
 : for each key attribute. Note that the function is variadic and might
 : take an arbitrary number of type identifiers for the key attributes.
 :
 : @param $name the name of the map
 : @param $key-type an arbitrary number of types, one
 :        for each key attribute.
 :
 : @return the function is sequential and immediately creates
 :         the corresponding map but returns the empty-sequence.
 :
 : @error err:XPTY0004 if any of the attribute types is not a subtype of
 :        xs:anyAtomicType.
 : @error zerr:ZSTR0001 if a map with the given name already exists.
 :)
declare %an:variadic %an:sequential function map:create-transient(
  $name as xs:QName,
  $key-type as xs:QName) as empty-sequence() external;

(:~
 : Destroys the map with the given name.
 :
 : @param $name the name of the map to delete
 :
 : @return the function is sequential and immediately deletes
 :         the map but returns the empty-sequence.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :)
declare %an:sequential function map:delete(
  $name as xs:QName) as empty-sequence() external;

(:~
 : Inserts a new entry into the map with the given name. Note that the
 : function is variadic and might take an arbitrary number of key attributes.
 : If an entry with the given key already exists in the map, the value
 : sequences of the existing entry and the sequence passed using $value
 : argument are concatenated.
 :
 : @param $name the name of the map
 : @param $value the value of the entry to insert
 : @param $key an arbitrary number of key attributes.
 :
 : @return the function is sequential and immediately inserts
 :         the entry into the map but returns the
 :         empty-sequence.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 : @error zerr:ZDDY0025 if the given number of key attributes does not match
 :        the number of key attributes specified when creating
 :        the map (see the map:create function).
 : @error zerr:ZXQD0005 if any of the given key attributes can not
 :        be cast (or is not a subtype) of the corresponding key attribute
 :        specified when creating the map.
 :
 : @see map:create
 :
 :)
declare %an:variadic %an:sequential function map:insert(
  $name as xs:QName,
  $value as item()*,
  $key as xs:anyAtomicType?) as empty-sequence() external;

(:~
 : Returns the value of the entry with the given key from the map.
 :
 : @param $name the name of the map
 : @param an arbitrary number of search key attributes.
 :
 : @return the value of the entry in the map identified
 :         by the given key. The empty-sequence will be returned
 :         if no entry with the given key is contained in the map.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 : @error zerr:ZDDY0025 if the given number of key attributes does not match
 :        the number of key attributes specified when creating
 :        the map (see the map:create function).
 : @error zerr:ZXQD0005 if any of the given key attributes can not
 :        be cast (or is not a subtype) of the corresponding key attribute
 :        specified when creating the map.
 :
 : @see map:create
 :)
declare %an:variadic function map:get(
  $name as xs:QName,
  $key as xs:anyAtomicType?) as item()* external;

(:~
 : Removes an entry identified by the given key from the map.
 :
 : @param $name the name of the map
 : @param an arbitrary number of search key attributes.
 :
 : @return the function is sequential and immediately removes
 :         the entry into the map but returns the
 :         empty-sequence.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 : @error zerr:ZDDY0025 if the given number of key attributes does not match
 :        the number of key attributes specified when creating
 :        the map (see the map:create function).
 : @error zerr:ZXQD0005 if any of the given key attributes can not
 :        be cast (or is not a subtype) of the corresponding key attribute
 :        specified when creating the map.
 :
 : @see map:create
 :
 :)
declare %an:variadic %an:sequential function map:remove(
  $name as xs:QName,
  $key as xs:anyAtomicType?) as empty-sequence() external;

(:~
 : Returns the keys of all entries of a map. The keys
 : are returned as sequence of nodes of the form:
 :
 :

 : <key xmlns="http://www.zorba-xquery.com/modules/store/data-structures/unordered-map">
 :   <attribute value="key1_value"/>
 :   <attribute value="key2_value"/>
 :   <attribute value="key3_value"/>
 : </key>
 :

 :
 : The following condition always holds:
 : <tt>map:size($name) eq fn:count(map:keys($name))</tt>
 :
 : @param $name the name of the map
 :
 : @return all keys in the map.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :
 :)
declare function map:keys(
  $name as xs:QName) as node()* external;

(:~
 : The number of entries in a map.
 :
 : The following condition always holds:
 : <tt>map:size($name) eq fn:count(map:keys($name))</tt>
 :
 : @param $name the name of the map
 :
 : @return the number of entries in the map.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :)
declare function map:size($name as xs:QName) as xs:integer external;

(:~
 : The function returns a sequence of QNames of the maps that are
 : available. The sequence will be empty if there are no maps.
 :
 : @return A sequence of QNames, one for each available map, or an emtpy sequence.
 :
 :)
declare function map:available-maps() as xs:QName* external;

(:~
 : The function returns true if the map identified by the given QName
 : is transient, false otherwise.
 :
 : @param $name the name of the map
 :
 : @return true if the map is transient, false otherwise.
 :
 : @error zerr:ZDDY0023 if a map with the given name does not exist.
 :)
declare function map:is-transient($name as xs:QName) as xs:boolean external;