xquery version "3.0";
(:
Processing Requests that are send in the BioCASE Protocol
:)
import module namespace biocase-lib = "http://exist-db.org/xquery/rebind/biocase-lib" at "biocase_lib.xql";
import module namespace biocase-settings = "http://exist-db.org/xquery/rebind/biocase-settings" at "biocase_settings.xql";
declare default element namespace "http://www.tdwg.org/schemas/abcd/2.06";
declare namespace biocase = "http://www.biocase.org/schemas/protocol/1.3";
declare namespace dsi = "http://www.biocase.org/schemas/dsi/1.0";
declare namespace abcd = "http://www.tdwg.org/schemas/abcd/2.06";
declare namespace query = "http://exist-db.org/xquery/rebind-query";
declare namespace request = "http://exist-db.org/xquery/request";
declare namespace util = "http://exist-db.org/xquery/util";
declare variable $local:debug-token as xs:double := util:random();
declare variable $local:debug-doc := doc($biocase-settings:debug-file);
declare variable $local:capabilities-doc := doc($biocase-settings:capabilities-file);
declare variable $local:collection := collection($biocase-settings:collection);
declare option exist:serialize "indent=yes method=xml media-type=application/xml omit-xml-declaration=no encoding=utf-8";
declare function local:log-debug-message($element as element()?) as node()*
{
let $update := if(
exists($local:debug-doc/biocase:root/biocase:diagnostics[@token=$local:debug-token])
)then(
)else(
update insert into $local:debug-doc/biocase:root
)
let $update := if(count($element)>0)then(update insert $element into $local:debug-doc/biocase:root/biocase:diagnostics[@token=$local:debug-token])else()
return $local:debug-doc
};
(: TODO make generic :)
declare function local:sort-by-datasets($elements as element()*) as element()*
{
let $datasets := $elements/ancestor::DataSet
return {
for $dataset in $datasets
let $datasetheaders := ($dataset/DatasetGUID,$dataset/TechnicalContacts,$dataset/ContentContacts,$dataset/OtherProviders,$dataset/Metadata)
return {$datasetheaders}{
for $unit in $elements
return if($unit/ancestor::DataSet eq $dataset)then( $unit ) else()
}
}
};
declare function local:sort-by-ancestors($elements as element()*, $namespace as xs:string) as element()*
{
let $root-node := $elements/root()/* (: ABCD equivalent: DataSets :)
let $root-attributes := $root-node/@*
let $child-nodes := $elements/ancestor::*[parent::* eq $root-node] (: ABCD equivalent: DataSet :)
let $child-nodes2 := for $child-node in $child-nodes
let $grandchildren := $elements/ancestor::*[parent::* eq $child-node] (: should be only one; ABCD equivalent: Units :)
let $header-grandchilden := $child-node/*[position() lt min($grandchildren/position())]
let $footer-grandchilden := $child-node/*[position() gt max($grandchildren/position())]
let $child-attributes := $child-node/@*
return element{$root-node/name()}{$root-attributes,$child-nodes}
let $return-root := element{$root-node/name()}{$root-attributes,$child-nodes}
return $return-root
};
(:declare function local:join-ancestors($elements as element()*, , $namespace as xs:string):)
declare function local:match-path($target-path-segments as xs:string*, $anchor-path-segments as xs:string*, $common-parent as xs:string?) as xs:string*{
if(exists($target-path-segments[1]) and exists($anchor-path-segments[1]))then(
if($target-path-segments[1] eq $anchor-path-segments[1])then(
local:match-path($target-path-segments[position() = 2 to last()],$anchor-path-segments[position() = 2 to last()], $target-path-segments[1])
)else(
let $ancestors := for $ancestor in reverse($anchor-path-segments[position() = 1 to last()-1])
return "parent::" || $ancestor || "/"
return if(exists($common-parent) and string-length($common-parent) gt 0)then(
string-join($ancestors) || "parent::"|| $common-parent || "/" || string-join($target-path-segments,"/")
)else(
let $debug-log := local:log-debug-message(Target path '/{string-join($target-path-segments,"/")}' and anchor path '/{string-join($anchor-path-segments,"/")}' do not have a common ancestor.)
return ""
)
)
)else if(exists($target-path-segments[1]) and not(exists($anchor-path-segments[1])))then(
string-join($target-path-segments,"/")
)else if(not(exists($target-path-segments[1])) and exists($anchor-path-segments[1]))then(
(: lets hope this segment is never used, because it means that there is an element with mixed content: text and child nodes :)
let $ancestors := for $ancestor in reverse($anchor-path-segments[position() = 1 to last()-1])
return "parent::" || $ancestor || "/"
return if(exists($common-parent) and string-length($common-parent) gt 0)then(
string-join($ancestors) || "parent::"|| $common-parent
)else(
let $debug-log := local:log-debug-message(Target path '/{string-join($target-path-segments,"/")}' and anchor path '/{string-join($anchor-path-segments,"/")}' do not have a common ancestor.)
return ""
)
)else if(not(exists($target-path-segments[1])) and not(exists($anchor-path-segments[1])))then(
"." (: this should not be reached since it is covered with the if($target-path eq $anchor-path) check in the move-path-anchor function :)
)else()
};
declare function local:move-path-anchor($target-path as xs:string, $anchor-path as xs:string) as xs:string
{
let $end := if(contains($target-path,"@"))then("/xs:string(.)")else("/text()")
return if($target-path eq $anchor-path)then(
"."||$end
)else(
let $target-path-segments := tokenize($target-path, "/")
let $anchor-path-segments := tokenize($anchor-path, "/")
return local:match-path($target-path-segments,$anchor-path-segments,"") || $end
)
};
declare function local:build-scan-response($query as node()) as element()?
{
let $request-format := $query/biocase:request/biocase:scan/biocase:requestFormat/text()
let $valid-request-format := if(contains($biocase-settings:namespaces,$request-format))then(true())else(
let $debug-log := local:log-debug-message(unsupported request format '{$request-format}')
return false()
)
let $concept := $query/biocase:request/biocase:scan/biocase:concept/text()
let $is-supported-concept := if(local:is-valid-concept($concept, $request-format))then(true())else(
let $debug-log := local:log-debug-message(unsupported scan concept '{$concept}')
return false()
)
return if($is-supported-concept and $valid-request-format)then(
let $concept-anchor := biocase-settings:get-concept-anchor($request-format)
let $filter := local:parse-filter($query/biocase:request/biocase:scan/biocase:filter, $concept, $request-format)
let $query := $concept || $filter
(:
if(starts-with($concept,$concept-anchor))then(
$concept || $filter || substring($concept,string-length($concept-anchor)+1)
)else(
$concept || "[ancestor::" || substring($concept-anchor,2) || $filter ||"]"
)
:)
let $query := "distinct-values(collection('"||$biocase-settings:collection||"')"||$query||")"
let $values := util:eval($query)
let $total-search-hits := count($values)
let $debug-log := local:log-debug-message({$query})
return
{for $value in $values
return {$value}
}
)else(())
};
declare function local:build-search-response($query as node()) as element()?
{
let $request-format := $query/biocase:request/biocase:search/biocase:requestFormat/text()
let $valid-request-format := if(contains($biocase-settings:namespaces,$request-format))then(true())else(
let $debug-log := local:log-debug-message(unsupported request format '{$request-format}')
return false()
)
let $response-format := $query/biocase:request/biocase:search/biocase:responseFormat/text()
let $identical-format := if($request-format eq $response-format)then(true())else(
let $debug-log := local:log-debug-message(Only identical request and response formats are supported. A conversion is not possible.)
return false()
)
return if($valid-request-format and $identical-format)then(
let $concept-anchor := biocase-settings:get-concept-anchor($request-format)
let $count := if(exists($query/biocase:request/biocase:search/biocase:count))then(xs:boolean($query/biocase:request/biocase:search/biocase:count))else(false())
let $filter := local:parse-filter($query/biocase:request/biocase:search/biocase:filter, $concept-anchor, $request-format)
let $xquery := "collection('"||$biocase-settings:collection||"')" || $concept-anchor|| $filter
let $units := util:eval($xquery)
let $record-start := $query/biocase:request/biocase:search/biocase:responseFormat/@start/xs:string(.)[1]
let $record-start := if(string(xs:integer($record-start)) != "NaN")then(xs:integer($record-start)+1)else(1)
let $record-count := $query/biocase:request/biocase:search/biocase:responseFormat/@limit/xs:string(.)[1]
let $record-count := if(string(xs:integer($record-count)) != "NaN")then(xs:integer($record-count))else(10)
let $total-search-hits := count($units)
let $limited-units := $units[position() = $record-start to $record-start + $record-count - 1]
let $final-record-count := count($limited-units)
let $sorted-return-data := if(not($count))then(local:sort-by-datasets($limited-units))else({$total-search-hits})
let $debug-log := local:log-debug-message({$xquery})
return
{$sorted-return-data}
)else(())
};
declare function local:build-capabilities-response() as element()?
{
let $now := current-dateTime()
let $duration := xs:dayTimeDuration($biocase-settings:capabilities-time-span)
let $dateTimeLimit := $now - $duration
return if(exists($local:capabilities-doc) and xs:dateTime($local:capabilities-doc/biocase:root/@date) gt $dateTimeLimit)
then({$local:capabilities-doc/biocase:root/biocase:capabilities})
else({biocase-lib:build-capabilities-report()})
};
declare function local:build-inventory-response() as element()?
{
let $now := current-dateTime()
let $title := $local:collection//abcd:Representation/abcd:Title/text()
return
OK{$now}
{$biocase-settings:biocase-access-point}request.xql
{for $dataset in collection($biocase-settings:collection)//abcd:DataSets
return
{$dataset//abcd:DataSet[1]/abcd:Metadata/abcd:Description/abcd:Representation/abcd:Title/text()}
{$dataset//abcd:DataSet[1]/abcd:Metadata/abcd:Description/abcd:Representation/abcd:Title/text()}
}
};
declare function local:parse-filter($query as node()?, $concept-anchor as xs:string, $namespace as xs:string) as xs:string*
{
let $result :=
if(exists($query) and $query/local-name() eq "filter" and exists($query/*[1]))
then("[" || local:parse-filter-fragment($query/*[1],$concept-anchor, $namespace) || "]")else("")
return $result
};
declare function local:is-valid-concept($concept as xs:string, $namespace as xs:string) as xs:boolean
{
(: TODO maybe trigger new capabilities report, if not current anymore :)
exists($local:capabilities-doc/biocase:root/biocase:capabilities/biocase:SupportedSchemas[@namespace eq $namespace]/biocase:Concept[text() eq $concept])
};
(:declare function local:get-fragment-old($query as node()?, $concept-anchor as xs:string, $namespace as xs:string) as xs:string*
{
for $fragment in $query/*
return local:parse-filter-fragment($fragment, $concept-anchor, $namespace)
};:)
(:
local:parse-filter-fragment($query/*[1], $concept-anchor,$namespace) ||
" or " || local:parse-filter-fragment($query/*[2], $concept-anchor,$namespace)
:)
(: TODO
- simplify @path context
:)
declare function local:parse-filter-fragment($query as node()?, $concept-anchor as xs:string, $namespace as xs:string) as xs:string*
{
(:let $local-path := local:move-path-anchor($query/@path,$concept-anchor):) (: local:move-path-anchor('',$concept-anchor) works for or query :)
let $local-path := if(exists($query/@path)) then (local:move-path-anchor($query/@path,$concept-anchor)) else () (:else (local:move-path-anchor('',$concept-anchor)) LORNA ADDED:)
(: TODO split error handling :)
let $result := if(exists($query/@path) and not(local:is-valid-concept($query/@path,$namespace)))then(
(: unsupported concept :)
let $string-path := $query/@path/xs:string(.)
let $error := local:log-debug-message('{$string-path}' is not a valid concept)
return "false()"
)
(: logical operators :)
else if(local-name($query) eq "and") then("(" || local:parse-filter-fragment($query/*[1], $concept-anchor,$namespace) || " and " || local:parse-filter-fragment($query/*[2], $concept-anchor,$namespace) || ")")
(:else if(local-name($query) eq "or") then(local:parse-or-operator($query, $concept-anchor, $namespace)) :)
(: see video to do a nested for statement :)
(:else if(local-name($query) eq "or") then("(" || local:parse-or-operator($query, $concept-anchor, $namespace) || ")") :)
(:else if(local-name($query) eq "or") then("(" || local:parse-filter-fragment(local:get-child($query), $concept-anchor,$namespace) || ")"):)
(:else if(local-name($query) eq "or") then("(" || (for $fragment in $query/* return local:parse-filter-fragment($fragment, $concept-anchor,$namespace)) || " or " ||") "):)
else if(local-name($query) eq "or") then("(" || local:parse-child-or-nodes($query, $concept-anchor, $namespace, count($query/*)) ||") ")
(:else if(local-name($query) eq "or") then("(" || local:parse-filter-fragment($query/*[1], $concept-anchor,$namespace) ||
" or " || local:parse-filter-fragment($query/*[2], $concept-anchor,$namespace) ||") "):)
(:biocase-lib:like('10',UnitID/text()) or biocase-lib:like('9',UnitID/text())) ]:)
else if(local-name($query) eq "not") then("not(" || local:parse-filter-fragment($query/*[1], $concept-anchor,$namespace) || ")")
(: comparative operators :)
else if(local-name($query) eq "equals") then(local:parse-generic-comparison($query,"=",$concept-anchor))
else if(local-name($query) eq "notEquals") then("not(" || local:parse-generic-comparison($query,"=",$concept-anchor) || ")")
else if(local-name($query) eq "like") then("biocase-lib:like('" || biocase-lib:sanitize-input($query/text()) || "'," || $local-path || ")")
else if(local-name($query) eq "greaterThan") then(local:parse-generic-comparison($query,"gt",$concept-anchor))
else if(local-name($query) eq "greaterThanOrEquals") then(local:parse-generic-comparison($query,"ge",$concept-anchor))
else if(local-name($query) eq "lessThan") then(local:parse-generic-comparison($query,"lt",$concept-anchor))
else if(local-name($query) eq "lessThanOrEquals") then(local:parse-generic-comparison($query,"le",$concept-anchor))
else if(local-name($query) eq "isNull") then("not(" || "exists(" || $local-path || ")" || ")")
else if(local-name($query) eq "isNotNull") then("exists(" || $local-path || ")")
(: TODO fix using group by :)
else if(local-name($query) eq "in") then("exists("||$local-path||") and exists(biocase-lib:index-of-all(('"||string-join(biocase-lib:lower-case-all(biocase-lib:sanitize-inputs($query/biocase:value/text())),"', '")||"'),biocase-lib:lower-case-all("||$local-path||")))")
else if(local-name($query) eq "true") then("true()") (: TODO remove later, temporary fix for easy testing :)
else ("false()")
return $result
};
(:lorna try different cardinality for return type e.g. * ? :)
(: lorna look at the diagnostics for the original like when only have [1] and [2] and see how it should look like :)
(:
for $fragment in $query/like
(:This works but doesn't seem to return any results - fragment should be :)
(:return $fragment :)
let $result := concat(local:parse-filter-fragment($fragment, $concept-anchor,$namespace), '|| " or " ||')
(:return concat('("(" ||', $result, '|| " or " ')
return 5:)
:)
declare function local:parse-or-operator($query as node(), $concept-anchor as xs:string, $namespace as xs:string) as xs:string*
{
(:let $result := : the $fragment is or/like for $item at $count in $items for $fragment in $query/like :)
for $fragment in $query/*
let $result := local:parse-filter-fragment($fragment, $concept-anchor,$namespace)
return concat($result , '|| " or " ||')
};
declare function local:get-next-child($query as node(), $position as xs:int) as node()
{
$query/*[$position]
};
declare function local:parse-child-or-nodes-old($query as node(), $concept-anchor as xs:string, $namespace as xs:string, $position as xs:int) as xs:string
{
(: if the first iteration position is 3 so this will call parse-filter-fragment and return the expression for the third node :)
let $result := if ($position > 1) then (local:parse-filter-fragment($query/*[$position], $concept-anchor,$namespace) || " or ")
else ()
(:local:parse-filter-fragment(local:get-next-child($query, 2), $concept-anchor,$namespace):)
(: recursive call of this function on node 3,2,1 :)
return local:parse-child-or-nodes($query, $concept-anchor, $namespace, $position -1)
};
declare function local:parse-child-or-nodes($query as node(), $concept-anchor as xs:string, $namespace as xs:string, $position as xs:int) as xs:string
{
(: if the first iteration position is 3 so this will call parse-filter-fragment and return the expression for the third node :)
let $result := if ($position > 0) then (local:parse-filter-fragment($query/*[$position], $concept-anchor,$namespace) || " or ")
else
local:parse-filter-fragment($query/*[$position], $concept-anchor,$namespace)
(:let $call-again := if ($position = 3) then (local:parse-child-or-nodes($query, $concept-anchor, $namespace, $position -1)) else ():)
let $call-again := if ($position > 0) then (local:parse-child-or-nodes($query, $concept-anchor, $namespace, $position -1)) else ()
(:does position=3 but not pos=2, 1 :)
(:local:parse-filter-fragment(local:get-next-child($query, 2), $concept-anchor,$namespace):)
(: recursive call of this function on node 3,2,1 :)
return concat($result , $call-again)
};
(:
Unit[(biocase-lib:like('3',UnitID/text()) or ) ]
:)
(: 30th april afternoon TODO check the count fo the number of like nodes - it should be recursive where we pass get-NEXT-child(query, position) to the function
and we have a count
OR perhaps we can just use a nested FOR - check 4mins into video:)
declare function local:get-child($query as node()) as node()*
{
(:let $result := : the $fragment is or/like for $item at $count in $items for $fragment in $query/like :)
for $fragment in $query/*
return $fragment
};
declare function local:parse-generic-comparison($query as node(), $operator as xs:string, $concept-anchor as xs:string) as xs:string?
{
let $local-path := local:move-path-anchor($query/@path,$concept-anchor)
let $result := if(string(number(biocase-lib:sanitize-input($query/text()))) != "NaN")then(
"number(" || $local-path || ") " || $operator || " number('" || biocase-lib:sanitize-input($query/text()) || "')"
)else if($operator eq "=")then(
(: note: it is important to use '=' for comparison and not 'eq', since the left side of the comparison is a set of several items :)
"biocase-lib:lower-case-all(" || $local-path || ") = '" || lower-case(biocase-lib:sanitize-input($query/text())) || "'"
)else(
$local-path || " " || $operator || " '" || biocase-lib:sanitize-input($query/text()) || "'"
)
return $result
};
(:local:parse-filter-fragment($fragment, $concept-anchor, $namespace):)
(: look at parse-generic-comparison and do something similar :)
(:return result
else if(local-name($query) eq "or") then("(" || local:parse-filter-fragment($query/*[1], $concept-anchor,$namespace) ||
" or " || local:parse-filter-fragment($query/*[2], $concept-anchor,$namespace) ||") ")
(:let $result := :)
for $fragment in $query/*
(:return $fragment :)
let $result := ("(" || local:parse-filter-fragment($fragment, $concept-anchor,$namespace) ||
" or " || false() ||") ")
return $result
return concat ('("(" ||', $result, '|| " or " || false() ||") ")')
:)
(:How do I get this function to call local:parse-filter-fragment on all 9 fragments. maybe we could just get the number of children in query/*
and then append each for
:)
(: entry point :)
let $request := request:get-parameter('request', '')
let $query := if($request = '') then (
let $request := request:get-parameter('query', '')
return if($request = '') then (
let $data := request:get-data()
return $data
) else (
util:parse($request)
)
) else (
util:parse($request)
)
let $inventory := request:get-parameter('inventory', '')
let $request := request:get-parameter('request', '') (:for GET :)
let $type := $query/biocase:request/biocase:header/biocase:type/text()
let $type := if(string-length($type)>0)then($type)else('capabilities')
let $response := if($inventory = '1')then(
local:build-inventory-response()
) else if($type = "scan")then(
if(exists($query/biocase:request/biocase:scan))then(
(: scan :)
local:build-scan-response($query)
)else(
let $debug-log := local:log-debug-message(missing scan element in the request, switching to "capabilities" request instead)
(: alernative capabilities :)
return local:build-capabilities-response()
)
)else if($type = "search")then(
if(exists($query/biocase:request/biocase:search))then(
(: search :)
local:build-search-response($query)
)else(
let $debug-log := local:log-debug-message(missing search element in the request, switching to "capabilities" request instead)
(: alernative capabilities :)
return local:build-capabilities-response()
)
) else(
(: GET :)
if(contains($query, '3Ctype%3Escan'))then()
else (
(: capabilities :)
local:build-capabilities-response()
)
)
let $debug-ception := local:log-debug-message({$local:debug-token})
(: get the biocase diagnostics from the database :)
let $debug-info := local:log-debug-message(())/biocase:root/biocase:diagnostics[@token=$local:debug-token]/biocase:diagnostic
(: filter depending on the debug level :)
let $debug-info := if($biocase-settings:debug-level eq "debug")then($debug-info)
else if($biocase-settings:debug-level eq "info")then($debug-info[contains(("info","warn","error","fatal"), lower-case(@severity))])
else if($biocase-settings:debug-level eq "warn")then($debug-info[contains(("warn","error","fatal"), lower-case(@severity))])
else if($biocase-settings:debug-level eq "error")then($debug-info[contains(("error","fatal"), lower-case(@severity))])
else if($biocase-settings:debug-level eq "fatal")then($debug-info[contains(("fatal"), lower-case(@severity))])
else ($debug-info[contains(("error","fatal"), lower-case(@severity))])
return if($inventory = '1')then ($response)
else (
1.3-rebind
{current-dateTime()}
{$biocase-settings:biocase-access-point}
{if(count($debug-info[@severity eq "fatal"]) eq 0)then($type)else("error")}
{if(count($debug-info[@severity eq "fatal"]) eq 0)then($response)else()}
{if(count($debug-info) gt 0)then(
{$debug-info}
)else()}
)