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()} )