updated copyright notice

updated copyright notice

xquery version "3.0";

(:
 : Copyright 2006-2013 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 implements the file API EXPath specification available at http://expath.org/spec/file
 : @author Gabriel Petrovay, Matthias Brantner, Markus Pilman
 : @see http://expath.org/spec/file
 : @project EXPath/EXPath File Module
 :)
module namespace file = "http://expath.org/ns/file";

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

(:~
 : Appends a sequence of Base64 items as binary to a file. If the file pointed
 : by
$file
 does not exist, a new file will be created.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:append-binary( $file as xs:string,
                             $content as xs:base64Binary* )
  as empty-sequence() external;

(:~
 : Appends a sequence of string items to a file.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:append-text( $file as xs:string, $content as xs:string* )
  as empty-sequence() external;

(:~
 : Returns the last component from the
$path
, deleting any
 : trailing directory-separator characters. If
$path
 consists
 : entirely directory-separator characters, the empty string is returned. If
 :
$path
 is the empty string, the string
"."
 is returned,
 : signifying the current directory.
 :
 : No path existence check is made.
 :
 : @param $path A file path/URI.
 : @return The base name of this file.
 :)
declare function file:base-name( $path as xs:string )
  as xs:string external;

(:~
 : Returns the last component from the
$path
, deleting any
 : trailing directory-separator characters and the
$suffix
. If path
 : consists entirely directory-separator characters, the empty string is
 : returned. If path is the empty string, the string
"."
 is
 : returned, signifying the current directory.
 :
 : No path existence check is made.
 :
 : The
$suffix
 can be used for example to eliminate file extensions.
 :
 : @param $path A file path/URI.
 : @param $suffix A suffix which should get deleted from the result.
 : @return The base-name of $path with a deleted $suffix.
 :)
declare function file:base-name( $path as xs:string, $suffix as xs:string )
  as xs:string
{
  let $res := file:base-name($path)
  return
    if (fn:ends-with($res, $suffix) and $res ne ".") then
      fn:substring($res, 1, fn:string-length($res) - fn:string-length($suffix))
    else
      $res
};

(:~
 : Copies a file or a directory given a source and a destination path/URI.
 :
 : @param $source The path/URI of the file or directory to copy.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If the computed destination points to a file system item
 :    with a different type than the
$source
.
 : @error file:FOFL0003 If
$destination
 does not exist and it's
 :    parent directory does not exist either.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic %an:sequential
function file:copy( $source as xs:string, $destination as xs:string )
  as empty-sequence()
{
  if (file:exists($source)) then
    if (file:is-directory($source)) then
      file:copy-directory-impl($source, $destination)
    else
      file:copy-file-impl($source, $destination)
  else
    fn:error(xs:QName("file:FOFL0001"), fn:concat("The source path does not exists: ", $source))
};

(:~
 : Creates a directory.
 :
 : The operation is will create all the missing parent directories from the
 : given path.
 :
 : @param $dir The path/URI denoting the directory to be created.
 : @return The empty sequence.
 : @error file:FOFL0002 If the directory cannot be created because of an already
 :    existing file.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential function
file:create-directory( $dir as xs:string )
  as empty-sequence() external;

(:~
 : Deletes a file or a directory from the file system.
 :
 : If
$path
 points to a directory the directory will be deleted
 : recursively.
 :
 : @param $path The path/URI of the file or directory to delete.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$path
 path does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic %an:sequential
function file:delete( $path as xs:string )
  as empty-sequence()
{
  if (file:exists($path,false())) then
    if (not(file:is-symlink($path)) and file:is-directory($path)) then
      file:delete-directory-impl($path)
    else
      file:delete-file-impl($path)
  else
    fn:error(xs:QName("file:FOFL0001"), fn:concat("The path does not exists: ", $path))
};

(:~
 : This function returns the value of the operating system specific directory
 : separator. For example,
/
 on Unix-based systems and
\

 : on Windows systems.
 :
 : @return The operating system specific directory separator.
 :)
declare function file:directory-separator()
  as xs:string external;

(:~
 : This function is the converse of
file:base-name
. It returns a
 : string denoting the parent directory of the
$path
. Any trailing
 : directory-separator characters are not counted as part of the directory
 : name. If path is the empty string or contains no directory-separator string,
 :
"."
 is returned, signifying the current directory.
 :
 : No path existence check is made.
 :
 : @param $path The filename, of which the dirname should be get.
 : @return The name of the directory the file is in.
 :)
declare function file:dir-name( $path as xs:string )
  as xs:string external;

(:~
 : Tests if a path/URI is already used in the file system.
 :
 : @param $path The path/URI to test.
 : @param $follow-symlinks if true, follows symbolic links.
 : @return true if $path points to an existing file system item.
 :)
declare %an:nondeterministic
function file:exists( $path as xs:string, $follow-symlinks as xs:boolean )
  as xs:boolean external;

(:~
 : Tests if a path/URI is already used in the file system.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to an existing file system item;
 : for symbolic links, retuns true if the linked-to item exists.
 :)
declare %an:nondeterministic
function file:exists( $path as xs:string )
  as xs:boolean
{
  file:exists( $path, true() )
};

(:~
 : A helper function that performs a trivial (not complete) glob to regex
 : pattern translation.
 :
 : @param $pattern The glob pattern.
 : @return A regex pattern corresponding to the glob pattern provided.
 :)
declare function file:glob-to-regex( $pattern as xs:string )
  as xs:string
{
  let $pattern := fn:replace($pattern, '(\.|\[|\]|\\|/|\||\-|\^|\$|\?|\*|\+|\{|\}|\(|\))','\\$1')
  let $pattern := fn:replace($pattern, '\\\?', '.')
  let $pattern := fn:replace($pattern, '\\\*', '.*')
  return fn:concat( "^", $pattern, "$" )
};

(:~
 : Tests if a path/URI points to a directory. On Unix-based systems, the root
 : and the volume roots are considered directories.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to a directory; for symbolic
 : links, returns true if the linked-to item is a directory.
 :)
declare %an:nondeterministic
function file:is-directory( $path as xs:string )
  as xs:boolean external;

(:~
 : Tests if a path/URI points to a file.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to a file; for symbolic links,
 : returns true if the linked-to item is a file.
 :)
declare %an:nondeterministic
function file:is-file( $path as xs:string )
  as xs:boolean external;

(:~
 : Tests if a path/URI points to symbolic link.  This works on all Unix-based
 : systems as well as Windows Vista or later.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to a symbolic link.
 :)
declare %an:nondeterministic
function file:is-symlink( $path as xs:string )
  as xs:boolean external;

(:~
 : Retrieves the timestamp of the last modification of the file system item
 : pointed by the path/URI.
 :
 : @param $path The file system item to read the last modification
 :    timestamp from.
 : @return The date and time of the last modification of the item.
 : @error file:FOFL0001 If the
$path
 does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic function file:last-modified( $path as xs:string )
  as xs:dateTime external;

(:~
 : Lists the file system items in a certain directory.
 :
 : The operation is equivalent to calling:
 :
file:list($dir, fn:false())
.
 :
 : @param $dir The path/URI of the directory to retrieve the children from.
 : @return The sequence of names of the direct children.
 : @error file:FOFL0003 If
$dir
 does not point to an existing directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:list( $dir as xs:string )
  as xs:string* external;

(:~
 : Lists the file system items in a certain directory. The order of the items
 : in the resulting sequence is not defined. The "." and ".." items are not
 : returned. The returned paths are relative to the provided
$path
.
 :
 : If
$recursive
 evaluates to
fn:true()
, the operation
 : will recurse in the subdirectories.
 :
 : @param $dir The path/URI to retrieve the children from.
 : @param $recursive A boolean flag indicating whether the operation should be
 :    performed recursively.
 : @return A sequence of relative paths.
 : @error file:FOFL0003 If
$dir
 does not point to an existing directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:list( $path as xs:string, $recursive as xs:boolean )
  as xs:string*
{
  for $f in file:list($path)
  let $full := fn:concat($path, file:directory-separator(), $f)
  return (
    $f,
    if ($recursive and file:is-directory($full)) then
      for $child in file:list($full, $recursive)
      return fn:concat($f, file:directory-separator(), $child)
    else
      ()
  )
};

(:~
 : Lists all files matching the given pattern in a given directory.
 : The order of the items in the result is not defined.
 : The "." and ".." items are not considered for the match.
 : The file paths are relative to the provided $path.
 :
 : The pattern is a glob expression supporting:
 :

 :

*
 for matching any number of unknown characters

 :

?
 for matching one unknown character

 :

 :
 : @param $path The path/URI to retrieve the children from.
 : @param $recursive A boolean flag indicating whether the operation should be
 :    performed recursively.
 : @param $pattern The file name pattern.
 : @return A sequence of file names matching the pattern.
 : @error file:FOFL0003 If
$dir
 does not point to an existing directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:list( $path as xs:string, $recursive as xs:boolean,
                    $pattern as xs:string )
  as xs:string*
{
  for $file in file:list( $path, $recursive )
  let $name := file:base-name( $file )
  return
    if ( fn:matches( $name, file:glob-to-regex( $pattern ) ) ) then
      $file
    else
      ()
};

(:~
 : Moves a file or directory given a source and a destination paths/URIs.
 :
 : @param $source The path/URI of the file to move.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If
$source
 points to a directory and
 :
$destination
 points to an existing file.
 : @error file:FOFL0003 If
$destination
 does not exist and its
 : parent directory does not exist either.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:move( $source as xs:string, $destination as xs:string )
  as empty-sequence()
{
  file:copy( $source, $destination );
  file:delete( $source );
};

(:~
 : This function returns the value of the operating system specific path
 : separator. For example,
:
 on Unix-based systems and
;

 : on Windows systems.
 :
 : @return The operating system specific path separator.
 :)
declare function file:path-separator()
  as xs:string external;

(:~
 : Transforms a URI, an absolute path, or relative path to a native path on the
 : running platform.
 :
 : No path existence check is made.
 :
 : @param $path The uri or path to normalize.
 : @return The native path corresponding to
$path
.
 : @error file:FOFL9999 If an error occurs while trying to obtain the native
 : path.
 :)
declare function file:path-to-native( $path as xs:string )
  as xs:string external;

(:~
 : Transforms a file system path into a URI with the file:// scheme. If the
 : path is relative, it is first resolved against the current working
 : directory.
 :
 : No path existence check is made.
 :
 : @param $path The path to transform.
 : @return The file URI corresponding to
path
.
 :)
declare function file:path-to-uri( $path as xs:string )
  as xs:anyURI external;

(:~
 : Reads the content of a file and returns a Base64 representation of the
 : content.
 :
 : @param $file The file to read.
 : @return The content of the file as Base64.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-binary( $file as xs:string )
  as xs:base64Binary external;

(:~
 : Reads the content of a file using the specified encoding and returns a
 : string representation of the content.
 :
 : @param $file The file to read.
 : @param $encoding The encoding used when reading the file.
 : If compiled with ICU, then Zorba supports any encoding that ICU supports;
 : otherwise Zorba only supports ASCII and UTF-8.
 : The encoding parameter is case insensitive.
 : @return The content of the file as string.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL0006 If
$encoding
 is not supported.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text( $file as xs:string, $encoding as xs:string )
  as xs:string external;

(:~
 : Reads the content of a file and returns a string representation of the
 : content.
 :
 : The operation is equivalent to calling:
 :
file:read-text($file, "UTF-8")
.
 :
 : @param $file The file to read.
 : @return The content of the file as string.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text( $file as xs:string )
  as xs:string
{
  file:read-text( $file, "UTF-8" )
};

(:~
 : Reads the content of a file and returns a sequence of strings representing
 : the lines in the content of the file.
 :
 : The operation is equivalent to calling:
 :
file:read-text-lines($file, "UTF-8")
.
 :
 : @param $file The file to read.
 : @return The content of the file as a sequence of strings.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text-lines( $file as xs:string )
  as xs:string*
{
  file:read-text-lines( $file, "UTF-8" )
};

(:~
 : Reads the content of a file using the specified encoding and returns a
 : sequence of strings representing the lines in the content of the file.
 :
 : This implementation considers the LF (
) character as the line
 : separator. If a resulting line ends with the CR (
) character, this is
 : trimmed as well. This implementation will uniformly treat LF and CRLF as
 : line separators.
 :
 : @param $file The file to read.
 : @param $encoding The encoding used when reading the file.
 : If compiled with ICU, then Zorba supports any encoding that ICU supports;
 : otherwise Zorba only supports ASCII and UTF-8.
 : The encoding parameter is case insensitive.
 : @return The content of the file as a sequence of strings.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL0006 If
$encoding
 is not supported.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text-lines( $file as xs:string, $encoding as xs:string )
  as xs:string* external;

(:~
 : Transforms a relative path/URI into an absolute operating system path by
 : resolving it against the current working directory.
 :
 : No path existence check is made.
 :
 : @param $path The path/URI to transform.
 : @return The operating system file path.
 :)
declare function file:resolve-path( $path as xs:string )
  as xs:string external;

(:~
 : Retrieves the size of a file.
 :
 : @param $file The file get the size.
 : @return An integer representing the size in bytes of the file.
 : @error file:FOFL0001 If the
$file
 does not exist.
 : @error file:FOFL0004 If the
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic function file:size( $file as xs:string )
  as xs:integer external;

(:~
 : Writes a sequence of Base64 items as binary to a file.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:write-binary( $file as xs:string, $content as xs:base64Binary* )
  as empty-sequence() external;

(:~
 : Writes a sequence of strings to a file.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @param $encoding The character encoding to write $content as.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:write-text( $file as xs:string, $content as xs:string*,
                          $encoding as xs:string )
  as empty-sequence() external;

(:~
 : Writes a sequence of strings to a file using the UTF-8 character encoding.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:write-text( $file as xs:string, $content as xs:string* )
  as empty-sequence()
{
  file:write-text( $file, $content, "UTF-8" )
};

(:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::)

(:~
 : Copies a file given a source and a destination path/URI.
 :
 : @param $sourceFile The path/URI of the file to copy.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If the computed destination points to directory.
 : @error file:FOFL0003 If
$destination
 does not exist and it's
 :    parent directory does not exist either.
 : @error file:FOFL0004 If
$sourceFile
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:sequential
function file:copy-file-impl( $sourceFile as xs:string,
                              $destination as xs:string )
  as empty-sequence() external;

(:~
 : Copies a source directory recursively to a destination path/URI.
 :
 : @param $sourceDir The path/URI of the directory to copy.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If
$destination
 points to an existing file.
 : @error file:FOFL0003 If
$destination
 does not exist and it's
 :    parent directory does not exist either.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:nondeterministic %an:sequential
function file:copy-directory-impl( $sourceDir as xs:string,
                                   $destination as xs:string )
  as empty-sequence()
{
  if (file:is-file($destination)) then
    fn:error(xs:QName("file:FOFL0002"), fn:concat("The specified destination path already exists: ", $destination))
  else if (fn:not(file:exists($destination))) then
    let $dirname := file:dir-name($destination)
    return
      if (fn:not(file:exists($dirname))) then
        fn:error(xs:QName("file:FOFL0003"), fn:concat("The destination directory does not exist: ", $dirname))
      else
        {
          file:create-directory($destination);
          file:copy-directory-content($sourceDir, $destination)
        }
  else
    let $basename := file:base-name($sourceDir)
    let $newdir := fn:concat($destination, file:directory-separator(), $basename)
    return
      {
        file:create-directory($newdir);
        file:copy-directory-content($sourceDir, $newdir)
      }
};

(:~
 : This is an internal function that copies an entire source directory to an
 : destination directory. The caller to this function must make sure that both
 : the source and destination point to existing directories.
 :
 : @param $sourceDir The existing source directory.
 : @param $destinationDir The existing destination directory.
 : @return The empty sequence.
 :)
declare %private %an:nondeterministic %an:sequential
function file:copy-directory( $sourceDir as xs:string,
                              $destinationDir as xs:string )
  as empty-sequence()
{
  let $name := file:base-name($sourceDir)
  let $destDir := fn:concat($destinationDir, file:directory-separator(), $name)
  return
    {
      file:create-directory($destDir);

      for $item in file:list($sourceDir)
      let $fullSrcPath := fn:concat($sourceDir, file:directory-separator(), $item)
      let $fullDestPath := fn:concat($destDir, file:directory-separator(), $item)
      return
        if (file:is-directory($fullSrcPath)) then
          file:copy-directory($fullSrcPath, $fullDestPath)
        else
          file:copy($fullSrcPath, $fullDestPath)
    }
};

(:~
 : Copies the content of a given directory to an existing destination
 : directory.
 :
 : @param $sourceDir The path/URI of the directory to copy the content from.
 : @param $destination The destination directory path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0003 If
$destination
 directory does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:nondeterministic %an:sequential
function file:copy-directory-content( $sourceDir as xs:string,
                                      $destination as xs:string )
  as empty-sequence()
{
  if (file:is-directory($destination)) then
    for $item in file:list($sourceDir)
    let $fullPath := fn:concat($sourceDir, file:directory-separator(), $item)
    return
      file:copy($fullPath, $destination)
  else
    fn:error(xs:QName("file:FOFL0003"), fn:concat("The specified destination directory does not exist: ", $destination))
};

(:~
 : Deletes a file from the file system.
 :
 : @param $file The path/URI of the file to delete.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$file
 path does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:sequential
function file:delete-file-impl( $file as xs:string )
  as empty-sequence() external;

(:~
 : Deletes a directory from the file system.
 :
 : @param $dir The path/URI of the directory to delete.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$dir
 path does not exist.
 : @error file:FOFL0003 If
$dir
 does not point to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:nondeterministic %an:sequential
function file:delete-directory-impl( $dir as xs:string )
  as empty-sequence()
{
  for $item in file:list($dir)
  let $fullPath := fn:concat($dir, file:directory-separator(), $item)
  return
    if (file:is-directory($fullPath)) then
      file:delete-directory-impl($fullPath);
    else
      file:delete-file-impl($fullPath);

  file:delete-file-impl($dir)
};

(: vim:set et sw=2 ts=2: :)

xquery version "3.0";

(:
 : Copyright 2006-2009 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 implements the file API EXPath specification available at http://expath.org/spec/file
 : @author Gabriel Petrovay, Matthias Brantner, Markus Pilman
 : @see http://expath.org/spec/file
 : @project EXPath/EXPath File Module
 :)
module namespace file = "http://expath.org/ns/file";

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

(:~
 : Appends a sequence of Base64 items as binary to a file. If the file pointed
 : by
$file
 does not exist, a new file will be created.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:append-binary( $file as xs:string,
                             $content as xs:base64Binary* )
  as empty-sequence() external;

(:~
 : Appends a sequence of string items to a file.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:append-text( $file as xs:string, $content as xs:string* )
  as empty-sequence() external;

(:~
 : Returns the last component from the
$path
, deleting any
 : trailing directory-separator characters. If
$path
 consists
 : entirely directory-separator characters, the empty string is returned. If
 :
$path
 is the empty string, the string
"."
 is returned,
 : signifying the current directory.
 :
 : No path existence check is made.
 :
 : @param $path A file path/URI.
 : @return The base name of this file.
 :)
declare function file:base-name( $path as xs:string )
  as xs:string external;

(:~
 : Returns the last component from the
$path
, deleting any
 : trailing directory-separator characters and the
$suffix
. If path
 : consists entirely directory-separator characters, the empty string is
 : returned. If path is the empty string, the string
"."
 is
 : returned, signifying the current directory.
 :
 : No path existence check is made.
 :
 : The
$suffix
 can be used for example to eliminate file extensions.
 :
 : @param $path A file path/URI.
 : @param $suffix A suffix which should get deleted from the result.
 : @return The base-name of $path with a deleted $suffix.
 :)
declare function file:base-name( $path as xs:string, $suffix as xs:string )
  as xs:string
{
  let $res := file:base-name($path)
  return
    if (fn:ends-with($res, $suffix) and $res ne ".") then
      fn:substring($res, 1, fn:string-length($res) - fn:string-length($suffix))
    else
      $res
};

(:~
 : Copies a file or a directory given a source and a destination path/URI.
 :
 : @param $source The path/URI of the file or directory to copy.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If the computed destination points to a file system item
 :    with a different type than the
$source
.
 : @error file:FOFL0003 If
$destination
 does not exist and it's
 :    parent directory does not exist either.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic %an:sequential
function file:copy( $source as xs:string, $destination as xs:string )
  as empty-sequence()
{
  if (file:exists($source)) then
    if (file:is-directory($source)) then
      file:copy-directory-impl($source, $destination)
    else
      file:copy-file-impl($source, $destination)
  else
    fn:error(xs:QName("file:FOFL0001"), fn:concat("The source path does not exists: ", $source))
};

(:~
 : Creates a directory.
 :
 : The operation is will create all the missing parent directories from the
 : given path.
 :
 : @param $dir The path/URI denoting the directory to be created.
 : @return The empty sequence.
 : @error file:FOFL0002 If the directory cannot be created because of an already
 :    existing file.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential function
file:create-directory( $dir as xs:string )
  as empty-sequence() external;

(:~
 : Deletes a file or a directory from the file system.
 :
 : If
$path
 points to a directory the directory will be deleted
 : recursively.
 :
 : @param $path The path/URI of the file or directory to delete.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$path
 path does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic %an:sequential
function file:delete( $path as xs:string )
  as empty-sequence()
{
  if (file:exists($path,false())) then
    if (not(file:is-symlink($path)) and file:is-directory($path)) then
      file:delete-directory-impl($path)
    else
      file:delete-file-impl($path)
  else
    fn:error(xs:QName("file:FOFL0001"), fn:concat("The path does not exists: ", $path))
};

(:~
 : This function returns the value of the operating system specific directory
 : separator. For example,
/
 on Unix-based systems and
\

 : on Windows systems.
 :
 : @return The operating system specific directory separator.
 :)
declare function file:directory-separator()
  as xs:string external;

(:~
 : This function is the converse of
file:base-name
. It returns a
 : string denoting the parent directory of the
$path
. Any trailing
 : directory-separator characters are not counted as part of the directory
 : name. If path is the empty string or contains no directory-separator string,
 :
"."
 is returned, signifying the current directory.
 :
 : No path existence check is made.
 :
 : @param $path The filename, of which the dirname should be get.
 : @return The name of the directory the file is in.
 :)
declare function file:dir-name( $path as xs:string )
  as xs:string external;

(:~
 : Tests if a path/URI is already used in the file system.
 :
 : @param $path The path/URI to test.
 : @param $follow-symlinks if true, follows symbolic links.
 : @return true if $path points to an existing file system item.
 :)
declare %an:nondeterministic
function file:exists( $path as xs:string, $follow-symlinks as xs:boolean )
  as xs:boolean external;

(:~
 : Tests if a path/URI is already used in the file system.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to an existing file system item;
 : for symbolic links, retuns true if the linked-to item exists.
 :)
declare %an:nondeterministic
function file:exists( $path as xs:string )
  as xs:boolean
{
  file:exists( $path, true() )
};

(:~
 : A helper function that performs a trivial (not complete) glob to regex
 : pattern translation.
 :
 : @param $pattern The glob pattern.
 : @return A regex pattern corresponding to the glob pattern provided.
 :)
declare function file:glob-to-regex( $pattern as xs:string )
  as xs:string
{
  let $pattern := fn:replace($pattern, '(\.|\[|\]|\\|/|\||\-|\^|\$|\?|\*|\+|\{|\}|\(|\))','\\$1')
  let $pattern := fn:replace($pattern, '\\\?', '.')
  let $pattern := fn:replace($pattern, '\\\*', '.*')
  return fn:concat( "^", $pattern, "$" )
};

(:~
 : Tests if a path/URI points to a directory. On Unix-based systems, the root
 : and the volume roots are considered directories.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to a directory; for symbolic
 : links, returns true if the linked-to item is a directory.
 :)
declare %an:nondeterministic
function file:is-directory( $path as xs:string )
  as xs:boolean external;

(:~
 : Tests if a path/URI points to a file.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to a file; for symbolic links,
 : returns true if the linked-to item is a file.
 :)
declare %an:nondeterministic
function file:is-file( $path as xs:string )
  as xs:boolean external;

(:~
 : Tests if a path/URI points to symbolic link.  This works on all Unix-based
 : systems as well as Windows Vista or later.
 :
 : @param $path The path/URI to test.
 : @return true if $path points to a symbolic link.
 :)
declare %an:nondeterministic
function file:is-symlink( $path as xs:string )
  as xs:boolean external;

(:~
 : Retrieves the timestamp of the last modification of the file system item
 : pointed by the path/URI.
 :
 : @param $path The file system item to read the last modification
 :    timestamp from.
 : @return The date and time of the last modification of the item.
 : @error file:FOFL0001 If the
$path
 does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic function file:last-modified( $path as xs:string )
  as xs:dateTime external;

(:~
 : Lists the file system items in a certain directory.
 :
 : The operation is equivalent to calling:
 :
file:list($dir, fn:false())
.
 :
 : @param $dir The path/URI of the directory to retrieve the children from.
 : @return The sequence of names of the direct children.
 : @error file:FOFL0003 If
$dir
 does not point to an existing directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:list( $dir as xs:string )
  as xs:string* external;

(:~
 : Lists the file system items in a certain directory. The order of the items
 : in the resulting sequence is not defined. The "." and ".." items are not
 : returned. The returned paths are relative to the provided
$path
.
 :
 : If
$recursive
 evaluates to
fn:true()
, the operation
 : will recurse in the subdirectories.
 :
 : @param $dir The path/URI to retrieve the children from.
 : @param $recursive A boolean flag indicating whether the operation should be
 :    performed recursively.
 : @return A sequence of relative paths.
 : @error file:FOFL0003 If
$dir
 does not point to an existing directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:list( $path as xs:string, $recursive as xs:boolean )
  as xs:string*
{
  for $f in file:list($path)
  let $full := fn:concat($path, file:directory-separator(), $f)
  return (
    $f,
    if ($recursive and file:is-directory($full)) then
      for $child in file:list($full, $recursive)
      return fn:concat($f, file:directory-separator(), $child)
    else
      ()
  )
};

(:~
 : Lists all files matching the given pattern in a given directory.
 : The order of the items in the result is not defined.
 : The "." and ".." items are not considered for the match.
 : The file paths are relative to the provided $path.
 :
 : The pattern is a glob expression supporting:
 :

 :

*
 for matching any number of unknown characters

 :

?
 for matching one unknown character

 :

 :
 : @param $path The path/URI to retrieve the children from.
 : @param $recursive A boolean flag indicating whether the operation should be
 :    performed recursively.
 : @param $pattern The file name pattern.
 : @return A sequence of file names matching the pattern.
 : @error file:FOFL0003 If
$dir
 does not point to an existing directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:list( $path as xs:string, $recursive as xs:boolean,
                    $pattern as xs:string )
  as xs:string*
{
  for $file in file:list( $path, $recursive )
  let $name := file:base-name( $file )
  return
    if ( fn:matches( $name, file:glob-to-regex( $pattern ) ) ) then
      $file
    else
      ()
};

(:~
 : Moves a file or directory given a source and a destination paths/URIs.
 :
 : @param $source The path/URI of the file to move.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If
$source
 points to a directory and
 :
$destination
 points to an existing file.
 : @error file:FOFL0003 If
$destination
 does not exist and its
 : parent directory does not exist either.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:move( $source as xs:string, $destination as xs:string )
  as empty-sequence()
{
  file:copy( $source, $destination );
  file:delete( $source );
};

(:~
 : This function returns the value of the operating system specific path
 : separator. For example,
:
 on Unix-based systems and
;

 : on Windows systems.
 :
 : @return The operating system specific path separator.
 :)
declare function file:path-separator()
  as xs:string external;

(:~
 : Transforms a URI, an absolute path, or relative path to a native path on the
 : running platform.
 :
 : No path existence check is made.
 :
 : @param $path The uri or path to normalize.
 : @return The native path corresponding to
$path
.
 : @error file:FOFL9999 If an error occurs while trying to obtain the native
 : path.
 :)
declare function file:path-to-native( $path as xs:string )
  as xs:string external;

(:~
 : Transforms a file system path into a URI with the file:// scheme. If the
 : path is relative, it is first resolved against the current working
 : directory.
 :
 : No path existence check is made.
 :
 : @param $path The path to transform.
 : @return The file URI corresponding to
path
.
 :)
declare function file:path-to-uri( $path as xs:string )
  as xs:anyURI external;

(:~
 : Reads the content of a file and returns a Base64 representation of the
 : content.
 :
 : @param $file The file to read.
 : @return The content of the file as Base64.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-binary( $file as xs:string )
  as xs:base64Binary external;

(:~
 : Reads the content of a file using the specified encoding and returns a
 : string representation of the content.
 :
 : @param $file The file to read.
 : @param $encoding The encoding used when reading the file.
 : If compiled with ICU, then Zorba supports any encoding that ICU supports;
 : otherwise Zorba only supports ASCII and UTF-8.
 : The encoding parameter is case insensitive.
 : @return The content of the file as string.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL0006 If
$encoding
 is not supported.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text( $file as xs:string, $encoding as xs:string )
  as xs:string external;

(:~
 : Reads the content of a file and returns a string representation of the
 : content.
 :
 : The operation is equivalent to calling:
 :
file:read-text($file, "UTF-8")
.
 :
 : @param $file The file to read.
 : @return The content of the file as string.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text( $file as xs:string )
  as xs:string
{
  file:read-text( $file, "UTF-8" )
};

(:~
 : Reads the content of a file and returns a sequence of strings representing
 : the lines in the content of the file.
 :
 : The operation is equivalent to calling:
 :
file:read-text-lines($file, "UTF-8")
.
 :
 : @param $file The file to read.
 : @return The content of the file as a sequence of strings.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text-lines( $file as xs:string )
  as xs:string*
{
  file:read-text-lines( $file, "UTF-8" )
};

(:~
 : Reads the content of a file using the specified encoding and returns a
 : sequence of strings representing the lines in the content of the file.
 :
 : This implementation considers the LF (
) character as the line
 : separator. If a resulting line ends with the CR (
) character, this is
 : trimmed as well. This implementation will uniformly treat LF and CRLF as
 : line separators.
 :
 : @param $file The file to read.
 : @param $encoding The encoding used when reading the file.
 : If compiled with ICU, then Zorba supports any encoding that ICU supports;
 : otherwise Zorba only supports ASCII and UTF-8.
 : The encoding parameter is case insensitive.
 : @return The content of the file as a sequence of strings.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0004 If
$source
 points to a directory.
 : @error file:FOFL0006 If
$encoding
 is not supported.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic
function file:read-text-lines( $file as xs:string, $encoding as xs:string )
  as xs:string* external;

(:~
 : Transforms a relative path/URI into an absolute operating system path by
 : resolving it against the current working directory.
 :
 : No path existence check is made.
 :
 : @param $path The path/URI to transform.
 : @return The operating system file path.
 :)
declare function file:resolve-path( $path as xs:string )
  as xs:string external;

(:~
 : Retrieves the size of a file.
 :
 : @param $file The file get the size.
 : @return An integer representing the size in bytes of the file.
 : @error file:FOFL0001 If the
$file
 does not exist.
 : @error file:FOFL0004 If the
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:nondeterministic function file:size( $file as xs:string )
  as xs:integer external;

(:~
 : Writes a sequence of Base64 items as binary to a file.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:write-binary( $file as xs:string, $content as xs:base64Binary* )
  as empty-sequence() external;

(:~
 : Writes a sequence of strings to a file.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @param $encoding The character encoding to write $content as.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:write-text( $file as xs:string, $content as xs:string*,
                          $encoding as xs:string )
  as empty-sequence() external;

(:~
 : Writes a sequence of strings to a file using the UTF-8 character encoding.
 :
 : @param $file The path/URI of the file to write the content to.
 : @param $content The content to be serialized to the file.
 : @return The empty sequence.
 : @error file:FOFL0004 If
$file
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %an:sequential
function file:write-text( $file as xs:string, $content as xs:string* )
  as empty-sequence()
{
  file:write-text( $file, $content, "UTF-8" )
};

(:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::)

(:~
 : Copies a file given a source and a destination path/URI.
 :
 : @param $sourceFile The path/URI of the file to copy.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If the computed destination points to directory.
 : @error file:FOFL0003 If
$destination
 does not exist and it's
 :    parent directory does not exist either.
 : @error file:FOFL0004 If
$sourceFile
 points to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:sequential
function file:copy-file-impl( $sourceFile as xs:string,
                              $destination as xs:string )
  as empty-sequence() external;

(:~
 : Copies a source directory recursively to a destination path/URI.
 :
 : @param $sourceDir The path/URI of the directory to copy.
 : @param $destination The destination path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0002 If
$destination
 points to an existing file.
 : @error file:FOFL0003 If
$destination
 does not exist and it's
 :    parent directory does not exist either.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:nondeterministic %an:sequential
function file:copy-directory-impl( $sourceDir as xs:string,
                                   $destination as xs:string )
  as empty-sequence()
{
  if (file:is-file($destination)) then
    fn:error(xs:QName("file:FOFL0002"), fn:concat("The specified destination path already exists: ", $destination))
  else if (fn:not(file:exists($destination))) then
    let $dirname := file:dir-name($destination)
    return
      if (fn:not(file:exists($dirname))) then
        fn:error(xs:QName("file:FOFL0003"), fn:concat("The destination directory does not exist: ", $dirname))
      else
        {
          file:create-directory($destination);
          file:copy-directory-content($sourceDir, $destination)
        }
  else
    let $basename := file:base-name($sourceDir)
    let $newdir := fn:concat($destination, file:directory-separator(), $basename)
    return
      {
        file:create-directory($newdir);
        file:copy-directory-content($sourceDir, $newdir)
      }
};

(:~
 : This is an internal function that copies an entire source directory to an
 : destination directory. The caller to this function must make sure that both
 : the source and destination point to existing directories.
 :
 : @param $sourceDir The existing source directory.
 : @param $destinationDir The existing destination directory.
 : @return The empty sequence.
 :)
declare %private %an:nondeterministic %an:sequential
function file:copy-directory( $sourceDir as xs:string,
                              $destinationDir as xs:string )
  as empty-sequence()
{
  let $name := file:base-name($sourceDir)
  let $destDir := fn:concat($destinationDir, file:directory-separator(), $name)
  return
    {
      file:create-directory($destDir);

      for $item in file:list($sourceDir)
      let $fullSrcPath := fn:concat($sourceDir, file:directory-separator(), $item)
      let $fullDestPath := fn:concat($destDir, file:directory-separator(), $item)
      return
        if (file:is-directory($fullSrcPath)) then
          file:copy-directory($fullSrcPath, $fullDestPath)
        else
          file:copy($fullSrcPath, $fullDestPath)
    }
};

(:~
 : Copies the content of a given directory to an existing destination
 : directory.
 :
 : @param $sourceDir The path/URI of the directory to copy the content from.
 : @param $destination The destination directory path/URI.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$source
 path does not exist.
 : @error file:FOFL0003 If
$destination
 directory does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:nondeterministic %an:sequential
function file:copy-directory-content( $sourceDir as xs:string,
                                      $destination as xs:string )
  as empty-sequence()
{
  if (file:is-directory($destination)) then
    for $item in file:list($sourceDir)
    let $fullPath := fn:concat($sourceDir, file:directory-separator(), $item)
    return
      file:copy($fullPath, $destination)
  else
    fn:error(xs:QName("file:FOFL0003"), fn:concat("The specified destination directory does not exist: ", $destination))
};

(:~
 : Deletes a file from the file system.
 :
 : @param $file The path/URI of the file to delete.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$file
 path does not exist.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:sequential
function file:delete-file-impl( $file as xs:string )
  as empty-sequence() external;

(:~
 : Deletes a directory from the file system.
 :
 : @param $dir The path/URI of the directory to delete.
 : @return The empty sequence.
 : @error file:FOFL0001 If the
$dir
 path does not exist.
 : @error file:FOFL0003 If
$dir
 does not point to a directory.
 : @error file:FOFL9999 If any other error occurs.
 :)
declare %private %an:nondeterministic %an:sequential
function file:delete-directory-impl( $dir as xs:string )
  as empty-sequence()
{
  for $item in file:list($dir)
  let $fullPath := fn:concat($dir, file:directory-separator(), $item)
  return
    if (file:is-directory($fullPath)) then
      file:delete-directory-impl($fullPath);
    else
      file:delete-file-impl($fullPath);

  file:delete-file-impl($dir)
};

(: vim:set et sw=2 ts=2: :)


lmutil:findAllIndirectLinks() appears to work. Fixed bug in resolveTopicrefFromResolvedMap()

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

   DITA Link Management Utilities

   Utilities that support general DITA link management
   actions.

   Author: W. Eliot Kimber

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

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

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

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

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

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

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

(:~
 : Constructs a link item map for a direct link.
 :
 : @param link The link element
 : @return Link item map
 :)
declare function lmutil:constructLinkItemMap(
                        $link as element()) as map(*) {
  let $result := lmutil:constructLinkItemMap($link)
  return $result
};

(:~
 : Constructs a link item map
 :
 : @param link The link element
 : @param topicref For indirect links, the topicref that establishes
 :                 the key resolution context for the link. Not set for direct URI links.
 :                 Note that this topicref element must be from the resolved map, not the
 :                 original source map.
 :)
declare function lmutil:constructLinkItemMap(
                        $link as element(),
                        $topicref as element()?) as map(*) {
   let $linkItem :=
      map{'link': $link,
          'resolvedMap': if ($topicref) then root($topicref)/* else (),
          'topicref' : $topicref,
          'processingRole':
             if (df:isTopicRef($link))
                then if ($link/@processing-role)
                        then string($link/@processing-role)
                        else 'normal' (: Default for topicref :)
                else 'normal', (: non-topicref links :)
          'linkContext': lmutil:getLinkContext($link)
         }
    return $linkItem
};

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

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

   The use parameters are:

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

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

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

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

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

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

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

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

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

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

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

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

};

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

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

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

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

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

};

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

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

};

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

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

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

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

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

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

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

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

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

   :)

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

   let $linksFromTopics := lmutil:getIndirectLinksFromTopics($rootMaps)

    let $linksFromMaps := () (: FIXME: Build this list as well :)
    let $links := ($linksFromTopics, $linksFromMaps)

   return $links
};

(:~
 : Get all the indirect links in topics referenced from the root maps
 :
 : @para rootMaps list of root content maps.
 :
 : @return Sequence, possibly empty, of link item maps.
 :)
declare function lmutil:getIndirectLinksFromTopics($rootMaps as element()*) as map(*)* {
  let $result :=
     for $map in $rootMaps
         let $resolvedMap as element() := lmutil:getResolvedMapForMap($map)
         let $bos as map(*)* := lmutil:getMapBoundedObjectSet($resolvedMap, ('dita'))
         return for $member in $bos
            return lmutil:getIndirectLinks($bos)
  return $result
};

(:~
 : Takes a sequence of map BOS members and, for each member,
 : finds the key-based links in the member and returns a of link
 : structures reflecting those links.
 :
 : @param bos Sequence of maps, one for each use of a topic from a
 : DITA map. Each map has two members, "topicref", and "resource"
 : @return A sequence, possibly empty, of link maps.
 :)
declare function lmutil:getIndirectLinks($bos as map(*)*) as map(*)* {
   let $result as map(*)* :=
       for $member in $bos
           return lmutil:getIndirectLinksForBOSMember($member)
   return $result
};

(:~
 : Given a BOS member map for a topic, finds all key-based links in the
 : topic and returns a sequence of link items for the links.
 :
 : @param member BOS member map containing a topic BOS member.
 : @return A sequence, possibly empty, of link items.
 :)
declare function lmutil:getIndirectLinksForBOSMember($member as map(*)) as map(*)* {
   let $topicref := $member('topicref')
   let $topic := $member('resource')
   let $linkElems := $topic//*[@keyref]
   let $linkItems :=
       for $elem in $linkElems
           return lmutil:constructLinkItemMap($elem, $topicref)
   return $linkItems
};

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

};

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

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

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

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

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

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

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

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

(: End of Module :)

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

   DITA Link Management Utilities

   Utilities that support general DITA link management
   actions.

   Author: W. Eliot Kimber

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

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

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

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

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

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

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

(:~
 : Constructs a link item map for a direct link.
 :
 : @param link The link element
 : @return Link item map
 :)
declare function lmutil:constructLinkItemMap(
                        $link as element()) as map(*) {
  let $result := lmutil:constructLinkItemMap($link)
  return $result
};

(:~
 : Constructs a link item map
 :
 : @param link The link element
 : @param topicref For indirect links, the topicref that establishes
 :                 the key resolution context for the link. Not set for direct URI links.
 :                 Note that this topicref element must be from the resolved map, not the
 :                 original source map.
 :)
declare function lmutil:constructLinkItemMap(
                        $link as element(),
                        $topicref as element()?) as map(*) {
   let $linkItem :=
      map{'link': $link,
          'resolvedMap': if ($topicref) then root($topicref)/* else (),
          'processingRole':
             if (df:isTopicRef($link))
                then if ($link/@processing-role)
                        then string($link/@processing-role)
                        else 'normal' (: Default for topicref :)
                else 'normal', (: non-topicref links :)
          'linkContext': lmutil:getLinkContext($link)
         }
    return $linkItem
};

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

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

   The use parameters are:

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

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

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

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

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

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

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

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

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

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

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

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

};

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

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

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

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

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

};

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

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

};

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

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

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

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

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

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

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

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

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

   :)

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

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

    let $links := $linksFromTopics

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

(:~
 : Takes a sequence of map BOS members and, for each member,
 : finds the key-based links in the member and returns a of link
 : structures reflecting those links.
 :
 : @param bos Sequence of maps, one for each use of a topic from a
 : DITA map. Each map has two members, "topicref", and "resource"
 : @return A sequence, possibly empty, of link maps.
 :)
declare function lmutil:getIndirectLinks($bos as map(*)) as map(*)* {
   let $result as map(*)* :=
       for $member in $bos
           return lmutil:getIndirectLinksForBOSMember($member)
   return $result
};

(:~
 : Given a BOS member map for a topic, finds all key-based links in the
 : topic and returns a sequence of link items for the links.
 :
 : @param member BOS member map containing a topic BOS member.
 : @return A sequence, possibly empty, of link items.
 :)
declare function lmutil:getIndirectLinksForBOSMember($member as map(*)) as map(*)* {
   let $topicref := $member('topicref')
   let $topic := $member('resource')
   let $linkElems := $topic//*[@keyref]
   let $linkItems :=
       for $elem in $linkElems
           return lmutil:constructLinkItemMap($elem, $topicref)
   return $linkItems
};

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

};

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

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

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

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

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

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

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

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

(: End of Module :)