R/Lims.R

   
#' Reference class for Lims instance
#'
#' @family Lims
#' @field version character Genologics LIMS version
#' @field baseuri character the base uri
#' @field auth httr::authenticate object for LIMS
#' @field fileauth httr::authenticate object for filestore
#' @field handle httr::handle object - accessor function provides a fresh handle
#'   for each transaction, but see \url{http://rstudio-pubs-static.s3.amazonaws.com/64194_2282137119ca48e1893054091456fe43.html#on-re-using-handles}
#' @field max_requests 4 element named numeric vector specifying the maximum
#'  number of requests per batch operation.  Defaults are below.  Set values to 
#'  1 to disable batch operations for that namespace.
#' \itemize{
#'      \item{artifacts = 300}
#'      \item{containers = 50}
#'      \item{samples = 300}
#'      \item{files = 100}
#'  }
#' @include Node.R
#' @export
LimsRefClass <- setRefClass('LimsRefClass',
   fields = list(
      version = 'character',
      baseuri = 'character',
      encoding = 'character',
      auth = 'ANY',
      fileauth = 'ANY',
      handle = 'ANY',
      max_requests = 'numeric',
      timeout = 'integer'),
   methods = list(
     initialize = function(){
        .self$field("max_requests", 
            c(artifacts = 300, 
            containers = 50, 
            samples = 300,
            files = 100))
    })
    
)  


#' Print a pretty summary
#' @name LimsRefClass_show Sample
#' @param prefix character - perhaps number of spaces to prefix the output 
NULL
LimsRefClass$methods(
   show = function(prefix = ""){
      cat(prefix, "Reference Class:", methods::classLabel(class(.self)), "\n", sep = "")
      cat(prefix, "  Lims version: ", .self$version, "\n", sep = "")
      cat(prefix, "  Lims baseuri: ", .self$baseuri, "\n", sep = "")
      cat(prefix, "  Lims valid session: ", .self$validate_session(), "\n", sep = "")
      nm <- names(.self$max_requests)
      s <- paste(paste0(nm, " = ", .self$max_requests), collapse = ", ")
      cat(prefix, "  Lims max_requests: ", s, "\n", sep = "")
   }) # show


#' Build a uri staring with the baseuri
#'
#' @family Lims
#' @name LimsRefClass_uri
#' @param ... one or more character segments to append to the base
#' @param base character, the base uri, if NULL then use this object's baseuri property
#' @return character uri
#' @examples 
#' \dontrun{
#'    my_uri <- ref$uri("containers")
#' }
NULL
LimsRefClass$methods(
   uri = function(..., base = NULL){
      stub = if(is.null(base)) .self$baseuri else base[1]
      file.path(stub, ...)
   })


#' Modify the URL provided to point to the named server
#'
#' @family LIMS
#' @name LimsRefClass_name_host
#' @param x a character URL like "http://localhost:9080/api/v2/processes/24-2792440"
#' @return an updated URL the explicitly names the host
NULL
LimsRefClass$methods(
    name_host = function(x = "http://localhost:9080/api/v2/processes/24-2792440"){
        x = httr::parse_url(x)
        y = httr::parse_url(.self$baseuri)
        file.path(paste0(y$scheme,":/"), y$hostname, x$path)
    }
    )

  
#' Validate the session by testing the version
#' 
#' @family Lims
#' @name LimsRefClass_validate_session
#' @return logical, TRUE if OK
NULL
LimsRefClass$methods(
   validate_session = function(){
      ok <- TRUE
      x <- httr::GET(.self$baseuri, 
         encoding = .self$encoding, 
         handle = .self$handle,
         .self$auth)
      if (httr::status_code(x) != 200) {
         warning("response has non-200 status code")
         print(x)
         ok <- FALSE
      }
      ok
   })


#' Retrieve the user and password
#'
#' @family Lims
#' @name LimsRefClass_userpwd
#' @param what character to specify which authorization ('lims' or 'file')
#'    by default lims is returned
#' @return character vector of [username, password] or NULL
NULL
LimsRefClass$methods(
   userpwd = function(what = c("lims", "file")){
   Auth <- switch(tolower(what[1]),
      'file' = .self$fileauth,
      .self$auth)
      
   if (is.null(Auth)){
      up <- NULL
   } else {
      # we can't depend upon httr providing the same format of attributes
      if ("userpwd" %in% names(Auth)){
         up <- strsplit(Auth[['userpwd']],":", fixed = TRUE)[[1]]
      } else if ("options" %in% names(Auth)) {
         up <- strsplit(Auth[['options']][['userpwd']],":", fixed = TRUE)[[1]]
      } else{
         cat("LimsRefClass$userpwd: unable to retrieve credentials\n")
         up <- NULL
      }
   }
   invisible(up)
   })
   
   
#' Verify a response and return xmlNode, possible an exception node
#' 
#' @name LimsRefClass_check
#' @family Lims
#' @param rsp httr::response object
#' @param msg if an exception is encountered, attach this message (unless NULL)
#' @return XML::xmlNode
NULL
LimsRefClass$methods(
   check = function(rsp, msg = NULL){
      # with httr 1.1 httr::warn_for_status no longer returns a logical
      # instead it returns either the response (no warning) or a condition error
      # see https://cran.r-project.org/web/packages/httr/news.html
      # so now we switch to httr::http_error()
      #w <- httr::warn_for_status(rsp) 
      
      stat_code <- httr::status_code(rsp)
      ok <- stat_code %in% c(OK = 200, Created = 201, Accepted = 202)
      if (!ok){
          stat_info <- httr::http_status(stat_code)
          msg <- xml2::xml_text(httr::content(rsp, encoding = .self$encoding))
          x <- .self$create_exception(message = c(stat_info[['message']], msg), status = stat_code)
          return(invisible(x))
      }

      w <- httr::http_error(rsp)
      if (!is.logical(w)) {
         print(rsp)
         print(httr::content(rsp, as = "text", encoding = .self$encoding))
      }
      
      x <- try(httr::content(rsp, as = "text", encoding = .self$encoding))
      if (inherits(x, 'try-error')){
         x <- .self$create_exception(message = "error extracting response content")
         return(invisible(x))
      }
      x <- try(XML::xmlTreeParse(x, asText = TRUE, 
         encoding = .self$encoding, useInternalNodes = TRUE,
         fullNamespaceInfo = TRUE))
      if (inherits(x, "try-error")){
         x <- .self$create_exception(message = "error with xmlTreeParse")
         return(invisible(x))
      }
      
      x <- try(XML::xmlRoot(x))
      if (inherits(x, "try-error")){
         x <- .self$create_exception(message = "error parsing response content with xmlRoot")
      }
      
      invisible(x)
   }) # verify_response
  
#' BROWSE a URI in a browser if in interactive session
#'
#' @family Lims
#' @name LimsRefClass_BROWSE
#' @param x XML::xmlNode or NodeRefClass
#' @param ... further arguments for httr::BROWSE
NULL
LimsRefClass$methods(
   BROWSE = function(x, ...){
   
      if(!interactive()){
          cat("not an interactive session - BROWSE is disabled\n")
          return(invisible(NULL))
      }
      
      if (is_xmlNode(x)){
         uri <- trimuri(xml_atts(x)[['uri']])
      } else if (inherits(x, 'character')) {
         uri <- x[1]
      } else if (inherits(x, 'NodeRefClass')){
         uri <- x$uri
      }
      
      httr::BROWSE(uri, 
         ..., 
         .self$auth)
   })
     
#' Create an exception node
#'
#' @name LimsRefClass_create_exception
#' @param message chracter, some error message
#' @param status numeric, status code
#' @param asNode logical if TRUE return ExceptionRefClass
#' @return XML::xmlNode of ExceptionRefClass
LimsRefClass$methods(
   create_exception = function(message = 'Unspecified exception',
        status = -1,
        asNode = FALSE){
      #x <- XML::newXMLNode("exception", 
      #   namespaceDefinitions = get_NSMAP()[['exc']], 
      #   namespace = 'exc')
      #x <- XML::addChildren(x, kids = list(XML::newXMLNode("message", message)) )
      x <- create_exception_node(message = message, status = status)
      if (asNode) x <- parse_node(x, .self)
      x
   }) #create_exception


#' LIST the URI's a resource with qualifiers
#'
#' @name LimsRefClass_LIST
#' @param resource character the uri to get
#' @param n numeric, the maximum number of URI, NA to get all
#' @param ... further arguments for httr::GET including \code{query} list
#' @return character vector of zero or more URI
#' @examples
#' \dontrun{
#'     # list the samples in a project
#'     ss <- lims$LIST('samples', projectname = 'foobar')
#' }
NULL
LimsRefClass$methods(
   LIST = function(resource, n = NA, ...){   
      list_resource(.self, resource, n = n, ...)
   }) #LIST

#' GET a resource, a wrapper around get_uri
#'
#' @name LimsRefClass_GET
#' @param uri character the uri to get
#' @param ... further arguments for httr::GET including \code{query} list
#' @param depaginate logical, if TRUE then pool paginated nodes into one
#' @param asNode logical, if TRUE return a class that inherits NodeRefClass 
#' @return XML::xmlNode - possibly an error node
#' @examples
#' \dontrun{
#'     # list the samples in a project - returns an XML::xmlNode
#'     ss <- lims$GET(lims$uri("samples"), query = list(projectname = 'foobar'), asNode = FALSE)
#'     # get the samples in a project parsed into as list of NodeRefClassObjects
#'     SS <- lims$GET(lims$uri("samples"), query = list(projectname = 'foobar'), asNode = TRUE)
#' }
NULL
LimsRefClass$methods(
   GET = function(uri=.self$baseuri, ..., depaginate = TRUE, asNode = TRUE){
      x <- get_uri(uri, .self, ..., depaginate = depaginate)
      if (asNode) x <- parse_node(x, .self) 
      invisible(x)
   }) #GET

#' PUT a resource
#'
#' @family Lims
#' @name LimsRefClass_PUT
#' @param x xmlNode to put
#' @param ... further arguments for httr::PUT
#' @return xmlNode
NULL
LimsRefClass$methods(
   PUT = function(x, ...){
      if (missing(x)) {
        cat("LimsRefClass$PUT: node is required\n")
        return(invisible(NULL))
      }
      
      if (inherits(x, "NodeRefClass")){
         uri <- x$uri
         body <- x$toString()
      } else if (is_xmlNode(x)) {
         uri <- trimuri(xml_atts(x)[['uri']])
         body <- xml_string(x)
      } else {
         cat("LimsRefClass$PUT: x must be xmlNode or NodeRefClass\n")
         return(NULL)
      }
      r <- httr::PUT(uri,  
         ..., 
         body = body, 
         httr::content_type_xml(), 
         handle = .self$handle,
         .self$auth) 
      .self$check(r)
   }) # PUT


#' POST a resource
#'
#' @family Lims
#' @name LimsRefClass_POST
#' @param x XML::xmlNode to or NodeRefClass POST
#' @param uri character if NULL taken from \code{x}
#' @param asNode logical, if TRUE return a class that inherits NodeRefClass.
#'    This should be FALSE for batch processing. 
#' @param ... further arguments for httr::POST
#' @return XML::xmlNode or NodeRefClass
NULL
LimsRefClass$methods(
   POST = function(x, uri = NULL, asNode = FALSE, ...){
      if (missing(x)) {
         cat("LimsRefClass$POST x as XML::xmlNode or NodeRefClass is required\n")
         return(NULL)
      }
      if (inherits(x, 'NodeRefClass')){
         if (is.null(uri)) uri <- x$uri
         body <- x$toString()
      } else {
         if (is.null(uri)) uri <- trimuri(xml_atts(x)[['uri']])
         body <- XML::toString.XMLNode(x)
      }
      r <- httr::POST(uri, 
         ..., 
         body = body, 
         httr::content_type_xml(),
         handle = .self$handle,
         .self$auth) 
      r <- .self$check(r)
      if (asNode) r <- try(parse_node(r, .self))
      r
   }) # POST
   
#' DELETE a resource
#' 
#' Typically this is a file (resource = 'files')
#' 
#' @family Lims
#' @name LimsRefClass_DELETE
#' @param x NodeRefClass, XML::xmlNode or character uri to DELETE, generally a file node
#' @param ... further arguments for httr::DELETE
#' @return logical
NULL
LimsRefClass$methods(
   DELETE = function(x, ...){
      if (missing(x)) {
        cat("LimsRefClass$DELETE node is required\n")
        return(NULL)
      }
      
      if (inherits(x, "NodeRefClass")){
         uri <- x$uri
      } else if (is_xmlNode(x)) {
         uri <- trimuri(xml_atts(x)[['uri']])
      } else if (inherits(x, 'character')) {
         uri <- trimuri(x)
      } else {
         cat("LimsRefClass$DELETE: x must be xmlNode, character, or NodeRefClass\n")
         return(NULL)
      }
      r <- httr::DELETE(uri, 
         ...,
         handle = .self$handle,
         .self$auth)
      if (httr::status_code(r) != 204){
         warning("LimsRefClass$DELETE unknown issue")
         print(r)
         print(httr::content(r))
      }
      invisible(httr::status_code(r) == 204)
   }) # POST

#' PUSH a file - not really a RESTful action but a combination of steps
#' 
#' given an artifact node and a filename
#' if the artifact has a file then 
#'    DELETE the file resource
#' create an unresolved file resource
#' POST the unresolved file resource to 'glsstore' to get a resolved file resource
#' Upload the file (scp, cp, or curl)
#' POST the resolved file resource to 'files'
#' return the resolved file resource
#      
#' @family Lims
#' @name LimsRefClass_PUSH
#' @param x ArtifactRefClass of the artifact to attach to 
#' @param ... further arguments for httr::GET/DELETE/POST
#' @param filename character, the fully qualified name of the file we are pushing
#'  Note that the caller must specify filename = 'some/file/name' explicitly.
#' @param use character the type of file transfer to use: duck, scp, cp or curl
#' @return XML::xmlNode or FileRefClass
NULL
LimsRefClass$methods(
   PUSH = function(x, ..., filename = "", 
      use = c("duck", "scp", "cp", "curl")[2]){
      
      stopifnot(inherits(x, 'ArtifactRefClass') || inherits(x, 'ProjectRefClass') )
      
      if (!file.exists(filename[1])){
          cat("LimsRefClass$PUSH file not found:", filename[1], "\n")
          return(NULL)
      }
      
      attached_to_uri <- trimuri(x[["uri"]])
      
      # if the artifact node has a file element
      # then we need to DELETE it
      if ( !is.null(x$node[["file"]]) ) {
         fileuri <- xml_atts(x$node[["file"]])["uri"]
         ok <- .self$DELETE(fileuri, ...)
         if (!ok) {
            e <- create_exception(message = "LimsRefClass$PUSH: Unable to delete existing file", asNode = TRUE)
            return(e)
         }
      }
      # create an unresolved file resource
      unresolved_node <- create_file_node(attached_to_uri, filename[1])
      # POST it
      uri <- .self$uri("glsstorage")
      body <- xmlString(unresolved_node)
      rbefore<- httr::POST(uri,
         body = body,
         httr::content_type_xml(),
         .self$auth)
      rbefore <- .self$check(rbefore)  
      
      if (is_exception(rbefore)){ return(rbefore)}
      resolved_node <- parse_node(rbefore, .self)
      
      
      # now we copy the file over...
      use <- tolower(use[1])
      dst <- resolved_node[['content_location']]
      up <- strsplit(.self$fileauth$options[['userpwd']], ":", fixed = TRUE)[[1]]
      puri <- httr::parse_url(resolved_node[['content_location']])

      ok <- 1
      if (use == "scp"){
          # first make the directory if it doesn't already exist
         MKDIR <- paste('ssh',
            paste0(up[1],'@',puri[['hostname']]), 
            shQuote(paste('mkdir -p', paste0("/", dirname(puri[['path']]) ) )) )
         ok <- system(MKDIR)
         if (ok == 0){
            # https://kb.iu.edu/d/agye
            # scp /path/to/source/file.txt dvader@deathstar.com:/path/to/dest/file.txt
            cmd <- paste('scp -q', filename[1], 
                paste0(up[[1]], "@", puri[['hostname']], ":/", puri[['path']] ))
            ok <- system(cmd)
         } else {
            e <- create_exception(message = "LimsRefClass$PUSH: Unable create destination path", asNode = TRUE)
            return(e)
         }
      } else if (use == "cp"){
         MKDIR <- paste('mkdir -p', paste0("/", dirname(puri[['path']]) ) )
         ok <- system(MKDIR)
         if (ok == 0){
             cmd <- paste("cp", shQuote(filename[1]),
                paste0("/", puri[['path']]) )
             ok <- system(cmd)
         } else {
            e <- create_exception(message = "LimsRefClass$PUSH: Unable create destination path", asNode = TRUE)
            return(e)
         }
      } else if (use == "curl"){
         cmd <- paste("curl --ftp-create-dirs",
            "-u", .self$fileauth[['options']][['userpwd']],
            "-T", filename[1],
            resolved_node[['content_location']])
         ok <- system(cmd)   
      } else if (use == 'duck'){
         up <- strsplit(.self$fileauth$options[['userpwd']], ":", fixed = TRUE)[[1]]
         ok <- duck_upload(filename[1], resolved_node[['content_location']],
            username = up[[1]], password = up[[2]])
      }
      if (ok != 0) {
         e <- create_exception(message = "LimsRefClass$PUSH: unable to upload file", asNode = TRUE)
         return(e)
      }
      
      uri <- .self$uri("files")
      body <- resolved_node$toString() 
      rafter <- httr::POST(uri,
         body = body,
         httr::content_type_xml(),
         .self$auth)
      rafter <- .self$check(rafter)  
      
      if (is_exception(rafter)){ return(rafter)}
      parse_node(rafter, .self)
   }) # PUSH


#' ATTACH a file - not really a RESTful action but a combination of steps
#'
#' Differs from PUSH as this is not placed into a genealogical placeholder.
#' but is simple attached to 'Files' tab if such exists as it does for Process
#' and Project.  Thus there is no DELETE involved like there might be with
#' a PUSH.
#' 
#' given an [Project,Artifact,Process] node and a filename
#' create an unresolved file resource
#' POST the unresolved file resource to 'glsstore' to get a resolved file resource
#' Upload the file (scp, cp, or curl)
#' POST the resolved file resource to 'files'
#' return the resolved file resource
#      
#' @family Lims
#' @name LimsRefClass_ATTACH
#' @param x ArtifactRefClass, ProcessRefClass or ProjectRefClass to attach to 
#' @param filename character, the fully qualified name of the file we are pushing
#'  Note that the caller must specify filename = 'some/file/name' explicitly.
#' @param use character the type of file transfer to use: duck, scp, cp or curl
#' @return FileRefClass, NULL or ExceptionRefClass
NULL
LimsRefClass$methods(
   ATTACH = function(x, filename = "", 
      use = c("duck", "scp", "cp", "curl")[2]){
      
      if (inherits(x, 'NodeRefClass')){
         if(!('ATTACH' %in% x$verbs)) {
            cat("Lims$ATTACH is not a verb of this class", class(x), "\n")
            return(invisible(NULL))
         }
      } else {
         cat("Lims$ATTACH input must inherit from NodeRefClass\n")
         return(invisible(NULL))
      }
      
      if (!file.exists(filename[1])) {
         cat("LimsRefClass$ATTACH file not found:", filename[1], "\n")
         return(NULL)
      }
      attached_to_uri <- trimuri(x[["uri"]])
      
      # create an unresolved file resource
      unresolved_node <- create_file_node(attached_to_uri, filename[1])
      # POST it
      uri <- .self$uri("glsstorage")
      body <- xmlString(unresolved_node)
      rbefore<- httr::POST(uri,
         body = body,
         httr::content_type_xml(),
         .self$auth)
      rbefore <- .self$check(rbefore)  
      
      if (is_exception(rbefore)){ return(rbeforer)}
      resolved_node <- parse_node(rbefore, .self)
      
      
      # now we copy the file over...
      use <- tolower(use[1])
      dst <- resolved_node[['content_location']]
      up <- strsplit(.self$fileauth$options[['userpwd']], ":", fixed = TRUE)[[1]]
      puri <- httr::parse_url(resolved_node[['content_location']])

      ok <- 1
      if (use == "scp"){
          # first make the directory if it doesn't already exist
         MKDIR <- paste('ssh',
            paste0(up[1],'@',puri[['hostname']]), 
            shQuote(paste('mkdir -p', paste0("/", dirname(puri[['path']]) ) )) )
         ok <- system(MKDIR)
         # https://kb.iu.edu/d/agye
         # scp /path/to/source/file.txt dvader@deathstar.com:/path/to/dest/file.txt
         cmd <- paste('scp -q', shQuote(filename[1]), 
            paste0(up[[1]], "@", puri[['hostname']], ":/", puri[['path']] ))
         ok <- system(cmd)
      } else if (use == "cp"){
         MKDIR <- paste('mkdir -p', paste0("/", dirname(puri[['path']]) ) )
         ok <- system(MKDIR)
         cmd <- paste("cp", shQuote(filename[1]),
            paste0("/", puri[['path']]) )
         ok <- system(cmd)
      } else if (use == "curl"){
         cmd <- paste("curl --ftp-create-dirs",
            "-u", .self$fileauth[['options']][['userpwd']],
            "-T", shQuote(filename[1]),
            resolved_node[['content_location']])
         ok <- system(cmd)   
      } else if (use == 'duck'){
         up <- strsplit(.self$fileauth$options[['userpwd']], ":", fixed = TRUE)[[1]]
         ok <- duck_upload(shQuote(filename[1]), resolved_node[['content_location']],
            username = up[[1]], password = up[[2]])
      }
      if (ok != 0) {
         # now what?
      }
      
      uri <- .self$uri("files")
      body <- resolved_node$toString() 
      rafter <- httr::POST(uri,
         body = body,
         httr::content_type_xml(),
         .self$auth)
      rafter <- .self$check(rafter)  
      
      if (is_exception(rafter)){ return(rafter)}
      parse_node(rafter, .self)
   }) # ATTACH





#' Retrieve a resource by limsid
#' 
#' @family Lims 
#' @name LimsRefClass_get_byLimsid
#' @param lismid character, one or more limsids
#' @param resource character, one resource to search, by default 'artifacts'
#' @return a list of NodeRefClass objects
NULL
LimsRefClass$methods(
   get_byLimsid = function(limsid, 
      resource = c("artifacts", "artifactgroups", "containers", "labs", "instruments", 
         "processes", "processtemplates", "projects", "researchers", "samples", 
         "configuration/udfs", "configuration/udts", "files")[1], ...){
      uri <- file.path(.self$baseuri, resource[1], limsid)
      lapply(uri, .self$GET, ...)
   })


#' Get one or more containers by name, state, etc
#' 
#' @family Lims Container
#' @name LimsRefClass_get_containers
#' @param optional name a character vector of one or more names
#' @param optional type character of one or more container types ("384 well plate", etc)
#' @param optional state character of one or more contain states ("Discarded", "Populated",...)
#' @param optional last_modified a character vector of last modification date in YYYY-MM-DDThh:mm:ssTZD format
#' @return a named list of ContainerRefClass or NULL
NULL
LimsRefClass$methods(
   get_containers = function(name = NULL, type = NULL, state = NULL,
   last_modified = NULL){
      resource <- 'containers'
      queryl = list()
      if (!is.null(name)) queryl[['name']] <- name
      if (!is.null(type)) queryl[['type']] <- type
      if (!is.null(state)) queryl[['state']] <- state
      if (!is.null(last_modified)) queryl[['last-modified']] <- last_modified
      if(length(queryl) == 0) {
         cat("LimsRefClass$get_containers please specify at least one or more of name, type, state or last_modified\n")
         return(NULL)
      }
      query <- build_query(queryl)
      x <- .self$GET(file.path(.self$baseuri, resource), query = query, asNode = FALSE)
      if (!is_exception(x)){
         if (length(XML::xmlChildren(x))==0) return(NULL)
         uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
         x <- batch_retrieve(uri, .self, rel = 'containers')
         x <- lapply(x, function(x) ContainerRefClass$new(x, .self))
         names(x) <- sapply(x, '[[', 'name')
      }
      if (inherits(x, "list")) class(x) <- append(class(x), "ContainerSet")
      invisible(x)
   })


#' Get or create containers by name and type
#' 
#' @name LimsRefClass_get_or_create_containers
#' @param name character one or more container names
#' @param ctype character one or more container types - only one type is allowed
#' @return a list of one or more ContainerRefClass or NULL
NULL
LimsRefClass$methods(
    get_or_create_containers = function(name = NULL, ctype = NULL){

    if (is.null(name)) {
        cat("name is required\n")
        return(NULL)
    }
    
    if (is.null(ctype)) {
        cat("ctype is required\n")
        return(NULL)
    }
    
    if (length(ctype) > 1){
        cat("only one ctype is permitted per call\n")
        return(NULL)
    }
    
    CC <- .self$get_containers(name = name, type = ctype)
    
    ix <- name %in% names(CC)

    if (any(!ix)){
        nm <- name[!ix]
        CType <- .self$get_containertypes(name = ctype)[[1]]
        if (is.null(CType)){
            cat("ctype not found:", ctype, "\n")
            return(NULL)
        }
        cc <- lapply(nm, function(n){
            create_container_node(CType$uri, name = n)
            })
        cc <- .self$batchcreate(cc)
        if (is.null(cc)){
            cat("error creating new containers", paste(nm, sep = " "), "\n")
            return(CC)
        }
        names(cc) <- sapply(cc, "[[", "name")
        CC <- c(CC, cc)[name]
    }
        
    CC

    })

#' Get the container type(s) in the system
#' 
#' 
#' @family Lims Container
#' @name LimsRefClass_get_containertypes
#' @param name a character vector of one or more container type names
#' @return a named list of ContainerTypeRefClass objects or NULL
NULL
LimsRefClass$methods(
   get_containertypes = function(name = NULL){
      queryl = list()
      if (!is.null(name)) queryl[['name']] <- name
      query <- build_query(queryl)
      x <- .self$GET(.self$uri("containertypes"), query = query, 
         depaginate = TRUE, asNode = FALSE)
      if (!is_exception(x) && length(x['container-type']) > 0){
         uris <- sapply(x['container-type'], function(x) xml_atts(x)[['uri']])
         names(uris) <- sapply(x['container-type'], function(x) xml_atts(x)[['name']])
         x <- lapply(uris, function(x) .self$GET(x))
      } else {
         x <- NULL
      }
      if (inherits(x, "list")) class(x) <- append(class(x), "ContainerTypeSet")
      invisible(x)
   })


#' Get artifact group(s) in the system
#' 
#' 
#' @family Lims
#' @name LimsRefClass_get_artifactgroups
#' @param artifactgroup a character vector of one or more artifact group names
#' @return a named list of ArtifactGroupTypeRefClass objects or NULL
NULL
LimsRefClass$methods(
   get_artifactgroups = function(artifactgroup = NULL){
      queryl = list()
      if (!is.null(artifactgroup)) queryl[['artifactgroup']] <- artifactgroup
      query <- build_query(queryl)
      x <- .self$GET(.self$uri("artifactgroups"), query = query, 
         depaginate = TRUE, asNode = FALSE)
      if (!is_exception(x) && length(x['artifactgroup']) > 0){
         uris <- sapply(x['artifactgroup'], function(x) xml_atts(x)[['uri']])
         #names(uris) <- sapply(x['artifactgroup'], function(x) xml_atts(x)[['name']])
         x <- lapply(uris, function(x) .self$GET(x))
         names(x) <- sapply(x, "[[", "name")
      } else {
         x <- NULL
      }
      if (inherits(x, "list")) class(x) <- append(class(x), "ArtifactGroupSet")
      invisible(x)
   })

#' Get one or more artifacts using queries on name, type, process-type, working-flag
#' qc-flag, sample-name, samplelimsid, containername, containerlimsid, reagent-label
#'
#' @family Lims Artifact
#' @name LimsRefClass_get_artifacts
#' @param name one or more artifact names or NULL to ignore
#' @param type character one or more character types or NULL to ignore
#' @param process_type character parent process type or NULL to ignore
#' @param working_flag character 'true' or 'false'  or NULL to ignore
#' @param qc_flag character on of UNKNOWN, PASSED, FAILED, CONTINUE or NULL to ignore
#' @param sample_name character one or more submitted sample names or NULL to ignore
#' @param samplelimsid character one or more submitted sample limsid  or NULL to ignore
#' @param artifactgroup character one or more experiment names  or NULL to ignore
#' @param container_name character one or more container names or NULL to ignore
#' @param containerlimsid character one or more container limsid  or NULL to ignore 
#' @param reagent-label character one or more reagent names or NULL to ignore
#' @return a list of ArtifactRefClass objects or NULL
NULL
LimsRefClass$methods(
   get_artifacts = function(name = NULL, type = NULL, process_type = NULL,
       working_flag = NULL,qc_flag = NULL,sample_name = NULL,samplelimsid = NULL,
       artifactgroup = NULL,container_name = NULL,containerlimsid = NULL,
       reagent_label = NULL){
       
      resource <- 'artifacts'
      query = list()
      if (!is.null(name)) query[['name']] <- name
      if (!is.null(type)) query[['type']] <- type
      if (!is.null(process_type)) query[['process-type']] <- process_type
      if (!is.null(working_flag)) query[['working-flag']] <- working_flag
      if (!is.null(qc_flag)) query[['qc-flag']] <- qc_flag
      if (!is.null(sample_name)) query[['sample-name']] <- sample_name
      if (!is.null(samplelimsid)) query[['samplelimsid']] <- samplelimsid
      if (!is.null(artifactgroup)) query[['artifactgroup']] <- artifactgroup
      if (!is.null(container_name)) query[['container-name']] <- container_name
      if (!is.null(containerlimsid)) query[['containerlimsid']] <- containerlimsid
      if (!is.null(reagent_label)) query[['reagent-label']] <- reagent_label
      if(length(query) == 0) {
         cat("LimsRefClass$get_artifacts please specify at least one or more of search parameters\n")
         return(NULL)
      }
      query <- build_query(query)
      
      x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE) 
      if (!is_exception(x)){
         if (length(XML::xmlChildren(x))==0) return(NULL)
         uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
         #x <- batch_retrieve(uri, .self, rel = 'artifacts')
         #x <- lapply(x, function(x) ArtifactRefClass$new(x, .self))
         x <- .self$batchretrieve(uri, rel = 'artifacts')
         names(x) <- sapply(x, '[[', 'name')
      }
      if (inherits(x, "list")) class(x) <- append(class(x), "ArtifactSet")
      invisible(x)

   }) # get_artifacts



#' Get one or more samples using queries on name, projectlimsid, projectname
#' It is possible to also filter the query on UDF values but it may be easier to
#  do that after getting the samples - see \url{http://genologics.com/developer}
#' 
#' @family Lims Sample
#' @name LimsRefClass_get_samples
#' @param optional name a character vector of one or more names
#' @param optional projectlimsid character of one or more projectlimsid values
#' @param optional projectname character of one or more projectname values
#' @return a named list of SampleRefClass or NULL
NULL
LimsRefClass$methods(
   get_samples = function(name = NULL, projectlimsid = NULL, projectname = NULL){
      resource <- 'samples'
      query = list()
      if (!is.null(name)) query[['name']] <- name
      if (!is.null(projectlimsid)) query[['projectlimsid']] <- projectlimsid
      if (!is.null(projectname)) query[['projectname']] <- projectname
      if(length(query) == 0) {
         cat("LimsRefClass$get_samples please specify at least one or more of name, projectlimsid or projectname\n")
         return(NULL)
      }
      query <- build_query(query)
      x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE)
      if (!is_exception(x)){
         uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
         len <- sapply(uri, length)
         if (all(len == 0)) return(NULL)
         if (.self$version == "v1"){
            x <- lapply(uri, function(x, lims=NULL) {lims$GET(x)}, lims = .self)
         } else {
            #x <- batch_retrieve(uri, .self, rel = 'samples')
            #x <- lapply(x, function(x) SampleRefClass$new(x, .self))
            x <- .self$batchretrieve(uri, rel = 'samples')   
         }
         names(x) <- sapply(x, '[[', 'name')
      }
      if (inherits(x, "list")) class(x) <- append(class(x), "SampleSet")
      invisible(x)
   })



#' Retrieve a list of WorkflowRefClass
#'
#' @name LimsRefClass_get_workflows
#' @param name character vector of one or more workflow names
#' @param form character of 'uri' or 'Node'
#' @return character vector or list with zero or more uri/WorkflowRefClass or NULL
NULL
LimsRefClass$methods(
    get_workflows = function(name = NULL, form = c('uri', 'Node')[2]){
        form <- tolower(form[1])
        resource = 'configuration/workflows'
        query <- if(is.null(name)) NULL else build_query(list(name=name))
        
        RR <- .self$GET(.self$uri(resource), query = query)
        rr <- RR$node['workflow']
        if (length(rr) == 0) return(NULL)
        aa <- lapply(rr, function(r) xml_atts(r))
        x <- sapply(aa, '[[', 'uri')
        names(x) <- sapply(aa, '[[', 'name')
        
        if (form == 'node') x <- lapply(x, function(u) .self$GET(u))
        x
    })


#' Retrieve a list of InstrumentRefClass or data.frame of the good stuff
#' @family Lims Instrument
#' @name LimsRefClass_get_instruments
#' @param optional name character a vector of one or more names
#' @param form character, return a 'data.frame' or list of Nodes
#' @return a list of  InstrumentRefClass, a data frame or NULL
NULL
LimsRefClass$methods(
   get_instruments = function(name = NULL, 
      form = c('data.frame', 'Node')[1]){
   
      resource <- 'instruments'
      
      query <- if(is.null(name)) NULL else build_query(list(name=name))
      
      RR <- .self$GET(.self$uri(resource), query = query)
      rr <- RR$node['instrument']
      if (length(rr) == 0) return(NULL)
      uri <- sapply(rr, function(x) xml_atts(x)[['uri']])
      x <- lapply(uri, function(x, lims = NULL) {
            lims$GET(x, asNode = TRUE)
         }, lims = .self)
      if (tolower(form[1]) == 'data.frame'){
         x <- data.frame (limsid = basename(uri),
            name = sapply(x, function(x) x$name),
            type = sapply(x, function(x) x$type),
            stringsAsFactors = FALSE)
      }
      if (inherits(x, "list")) class(x) <- append(class(x), "InstrumentSet")

      invisible(x)
   }) # get_instruments

#' Retrieve a list of ResearcherRefNodes or a data.frame of the good stuff
#' 
#' @family Lims Researcher
#' @name LimsRefClass_get_researchers
#' @param optional username character a vector of one or more user names like 'btupper' etc.
#' @param form character, return a 'data.frame' or list of Nodes
#' @return a list of  ResearcherRefClass, a data frame or NULL
NULL
LimsRefClass$methods(
   get_researchers = function(username = NULL, 
      form = c('data.frame', 'Node')[1]){
   
      resource <- 'researchers'
      
      query <- if(is.null(username)) NULL else build_query(list(username=username))
      
      RR <- .self$GET(.self$uri(resource), query = query)
      rr <- RR$node['researcher']
      if (length(rr) == 0) return(NULL)
      uri <- sapply(rr, function(x) xml_atts(x)[['uri']])
      x <- lapply(uri, function(x, lims = NULL) {
            lims$GET(x, asNode = TRUE)
         }, lims = .self)
      names(x) <- sapply(x, "[[", "name")
      if (tolower(form[1]) == 'data.frame'){
         x <- data.frame (limsid = basename(uri),
            name = sapply(x, function(x) x$name),
            username = sapply(x, function(x) x$username),
            initials = sapply(x, function(x) x$initials),
            email = sapply(x, function(x) x$email),
            credentials = sapply(x, function(x) x$get_credentials()),
            stringsAsFactors = FALSE)
      }
      invisible(x)
   }) # get_researchers


#' Get one or more Processes - does not leverage /batch/retrieve resources
#' but provides similar behavior.
#'
#' @family Lims Process
#' @name LimsRefClass_get_processes
#' @param last_modified optional character vector in YYYY-MM-DDThh:mm:ssTZD format
#' @param type optional character of the process type
#' @param inputartifactlimsid optional character of an input artifact limsid
#' @param technamefirst optional technician's first name
#' @param technamelast optional technician's last name
#' @param projectname optional project name
#' @return a list of ProcessRefClass or NULL
LimsRefClass$methods(
   get_processes = function(last_modified = NULL, type = NULL, 
      inputartifactlimsid = NULL, technamefirst = NULL, technamelast = NULL,
      projectname = NULL){
      
      resource <- 'processes'
      
      query <- list()
      if (!is.null(last_modified)) query[["last-modified"]] <- last_modified
      if (!is.null(type)) query[["type"]] <- type
      if (!is.null(inputartifactlimsid)) query[["inputartifactlimsid"]] <- inputartifactlimsid
      if (!is.null(technamefirst)) query[["technamefirst"]] <- technamefirst
      if (!is.null(technamelast)) query[["technamelast"]] <- technamelast
      if (!is.null(projectname)) query[["projectname"]] <- projectname
      if (length(query)>0) {
         query <- build_query(query)
      } else {
         query <- NULL
      }
   
      x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE)
      if (is_exception(x)){
          print(x)
          return(NULL)
      }
      if (length(XML::xmlChildren(x)) == 0) return(NULL)
      
      uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
      x <- lapply(uri, 
         function(x, lims = NULL) {
            lims$GET(x, asNode = TRUE)
         }, 
         lims = .self)
      if (inherits(x, "list")) class(x) <- append(class(x), "ProcessSet")
      
      invisible(x)
   }) # get_processes


#' Get one or more Labs - does not leverage /batch/retrieve resources
#' but provides similar behavior.
#'
#' @family Lims Labs
#' @name LimsRefClass_get_labs
#' @param name optional lab name
#' @param last_modified optional character vector in YYYY-MM-DDThh:mm:ssTZD format
#' @return a list of LabRefClass or NULL
LimsRefClass$methods(
   get_labs = function(name = NULL, last_modified = NULL){
      
      resource <- 'labs'
      
      query <- list()
      if (!is.null(last_modified)) query[["last-modified"]] <- last_modified
      if (!is.null(name)) query[["name"]] <- name
      if (length(query)>0) query <- build_query(query)
      if (length(query) == 0) query <- NULL
      
      x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE)
      if (length(XML::xmlChildren(x)) == 0) return(NULL)
      
      uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
      x <- lapply(uri, 
         function(x, lims = NULL) {
            lims$GET(x, asNode = TRUE)
         }, 
         lims = .self)
      names(x) <- sapply(x, '[[', 'name' )
      if (inherits(x, "list")) class(x) <- append(class(x), "LabSet")

      invisible(x)
   }) # get_labs


#' Get one or more Projects - does not leverage /batch/retrieve resources
#' but provides similar behavior.
#'
#' @family Lims Projects
#' @name LimsRefClass_get_projects
#' @param name optional project name
#' @param last_modified optional character vector in YYYY-MM-DDThh:mm:ssTZD format
#' @return a list of ProjectRefClass or NULL
LimsRefClass$methods(
   get_projects = function(name = NULL, last_modified = NULL){
      
      resource <- 'projects'
      
      query <- list()
      if (!is.null(last_modified)) query[["last-modified"]] <- last_modified
      if (!is.null(name)) query[["name"]] <- name
      if (length(query)>0) {
         query <- build_query(query)
      } else {
         query <- NULL
      }
   
      x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE)
      if (length(XML::xmlChildren(x)) == 0) return(NULL)
      
      uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
      x <- lapply(uri, 
         function(x, lims = NULL) {
            lims$GET(x, asNode = TRUE)
         }, 
         lims = .self)
      
      if (!any(sapply(x, is.null))) names(x) <- sapply(x, "[[", "name")
      if (inherits(x, "list")) class(x) <- append(class(x), "ProjectSet")
      
      invisible(x)
   }) # get_projects

#' Get one or more process-types as Nodes
#'
#' @family Lims Process
#' @name LimsRefClass_get_processtypes
#' @param displayname optional project name
#' @return a list of NodeRefClass or NULL
LimsRefClass$methods(
   get_processtypes = function(displayname = NULL){
      
      resource <- 'processtypes'
      
      query <- list()
      if (!is.null(displayname)) query[["displayname"]] <- displayname
      if (length(query)>0) {
         query <- build_query(query)
      } else {
         query <- NULL
      }
   
      x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE)
      if (length(XML::xmlChildren(x)) == 0) return(NULL)
      
      uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
      x <- lapply(uri, 
         function(x, lims = NULL) {
            lims$GET(x, asNode = TRUE)
         }, 
         lims = .self)
      names(x) <- sapply(x, '[[','name' )
      if (inherits(x, "list")) class(x) <- append(class(x), "ProcessTypeSet")

      invisible(x)
   }) # get_processtypes


#' Get one or more fields (UDF) as Nodes
#' 
#' @family Lims Field
#' @name LimsRefClass_get_fields
#' @param name one or more optional UDF names
#' @param attach_to_name one name of a container, sample, project, process
#' @param attach-to-category If 'attach_to_name' is the name of a process, 
#'  specify 'ProcessType'. Must not be provided otherwise. 
#' @param ... further arguments for GET method
#' @return a list of FieldRefClass or NULL
NULL
LimsRefClass$methods(
    get_fields = function(
        name = NULL, 
        attach_to_name = NULL, 
        attach_to_category = NULL,
        ...){
       
        resource <- 'configuration/udfs'
        
        queryl <- list()
        if (!is.null(name)) queryl[["name"]] <- name
        if (!is.null(attach_to_name)) queryl[["attach-to-name"]] <- attach_to_name
        if (!is.null(attach_to_category)) 
            queryl[["attach-to-category"]] <- attach_to_category
        query <- build_query(queryl)
        x <- .self$GET(.self$uri(resource), query = query, asNode = FALSE,...)
        if (length(XML::xmlChildren(x)) == 0) return(NULL)
        
        uri <- sapply(XML::xmlChildren(x), function(x) xml_atts(x)[['uri']])
        x <- lapply(uri, 
           function(x, lims = NULL) {
              lims$GET(x, asNode = TRUE)
           }, 
           lims = .self)
        names(x) <- sapply(x, '[[','name')
        if (inherits(x, "list")) class(x) <- append(class(x), "FieldSet")

        invisible(x)
    }) # get_fields
    


#' Get one or more nodes by uri by batch (artifacts, files, samples, containers only)
#' 
#' Return order is enforced to be the same as the input order
#'
#' @family Lims Node
#' @name LimsRefClass_batchretrieve
#' @param uri a vector of one or more uri for atomic entities in the GLS API
#' @param rel the relative name space into the "batch/retrieve" If not provided
#'  then it is detected from the first element of the input uri.
#' @param asNode logical, if TRUE parse to the appropriate node type
#' @param rm_dups logical, if TRUE then remove duplicates
#' @param ... further arguments for \code{batch_retrieve}
#' @return a list of XML::xmlNode or NodeRefClass objects
NULL
LimsRefClass$methods(
   batchretrieve = function(uri, 
      rel = c(NA, "artifacts", "samples", "containers", "files")[1], 
      rm_dups = TRUE, asNode = TRUE, ...){
      if (is.na(rel)) rel <- basename(dirname(uri[1]))
      if (!(rel[1] %in% c("artifacts", "samples", "containers", "files"))) {
         cat("LimsRefClass$batchretrieve rel must be one of artifacts, files, samples or containers\n")
         return(NULL)
      }
      if ((.self$get_max_requests(rel) <= 1 ) || 
        ((.self$version == "v1") && (rel %in% c('samples', 'files'))) ){
         x <- lapply(uri, function(x, lims=NULL) {lims$GET(x, asNode = FALSE)}, lims = .self)
      } else {
         uri2 <- split_vector(uri, MAX = .self$get_max_requests(rel))
         x <- unlist(lapply(uri2,
            function(x){
                batch_retrieve(x,.self, rel = rel[1], rm_dups = rm_dups)
            }))
         new_uri <- trimuri(sapply(x, function(x) xml_atts(x)[['uri']]))
         ix <- match(basename(uri), basename(new_uri))
         x <- x[ix]        
      }  
      if (asNode) {
         x <- lapply(x, parse_node, .self)
         names(x) <- switch(rel[1],
            'files' = names(x),
            sapply(x, function(x) xml_value(x$node[['name']]))  )
      }
      
      cl <- unname(c("artifacts" = 'ArtifactSet', "samples" = 'SampleSet', 
      "containers" = 'ContainerSet', "files"= 'FileSet')[rel])
      if (inherits(x, 'list')) class(x) <- append(class(x), cl)
      invisible(x)
   })



#' Update one or more Nodes (artifacts, samples, containers only)
#'
#' Return order is enforced to be the same as the input order
#'
#' @family Lims Node
#' @name LimsRefClass_batchupdate
#' @param x a list of one or mode XML::xmlNode of NodeRefClass
#' @param asNode logical, if TRUE return NodeRefClass objects otherwise XML::xmlNode
#' @param ... further arguments for httr::POST
#' @return a list of NodeRefClass or NULL
NULL
LimsRefClass$methods(
   batchupdate = function(x, asNode = TRUE, ...){
      if (!is.list(x)) x <- list(x)
      if (inherits(x[[1]], "NodeRefClass") ) {
         #origx <- x
         orig_uri <- sapply(x, "[[", "uri")
         x <- lapply(x, function(x) x$node)
      } else {
         orig_uri <- sapply(x, function(x) xml_atts(x)[['uri']])
      }
      ok <- sapply(x, is_xmlNode)
      if (!all(ok)) {
         cat("LimsRefClass$batchupdate: inputs must inherit xmlNode or NodeRefClass\n")
         return(NULL)
      }
      nm <- unique(sapply(x, xml_name))
      if (length(nm) > 1) {
         cat("LimsRefClass$batchupdate: all nodes must be of the same type - ", paste(nm, collapse = " "), "\n")
         return(NULL)
      }
      if (!(nm %in% c("artifact", "sample", "container"))){
         cat("LimsRefClass$batchupdate: only artifact, sample and container types have batch update\n")
         return(NULL)
      }
      
      xx <- split_vector(x, MAX = .self$get_max_requests(nm))
      
      rr <- lapply(xx, 
        function(x, lims = NULL, asNode = TRUE, rel = "") { 
                batch_update(x, lims, asNode = asNode, rel = rel)
            }, lims = .self, asNode = asNode, rel = plural(nm[1]))
            
      rr <- unlist(rr)
      
      if (asNode){
          new_uri <- sapply(rr, "[[", "uri")
      } else {
          new_uri <- trimuri(sapply(rr, function(x) xml_atts(x)[['uri']]))
      }
      
      ok <- basename(new_uri) %in% basename(orig_uri)
      
      if (!all(ok)){
          cat(sprintf("batch/update failed to retrieve %i inputs", length(sum(!ok))), "\n")
          return(NULL)
      } 
      
      ix <- match(basename(orig_uri), basename(new_uri))
      rr <- rr[ix]
      
      cl <- unname( c('ContainerRefClass' = 'ContainerSet',
        'SampleRefClass' = "SampleSet",
        "ArtifactRefClass" = 'ArtifactSet')[class(rr[[1]])])
      if (inherits(rr, 'list')) class(rr) <- append(class(rr), cl)
      invisible(rr)
   })

#' Create one or more Nodes (samples, containers only)
#'
#' Return order is *not* enforced to be the same as the input order
#'
#' @family Lims Node
#' @name LimsRefClass_batchcreate
#' @param x a list of one or more XML::xmlNode or NodeRefClass
#' @param asNode logical, if TRUE return NodeRefClass objects otherwise XML::xmlNode
#' @param ... further arguments for httr::POST
#' @return a list of NodeRefClass or NULL
NULL
LimsRefClass$methods(
   batchcreate = function(x, asNode = TRUE, ...){
      if (!is.list(x)) x <- list(x)
      if (inherits(x[[1]], "NodeRefClass") ) {
          
         x <- lapply(x, "[[", node)
      }
      ok <- sapply(x, is_xmlNode)
      if (!all(ok)) {
         cat("LimsRefClass$batchcreate: inputs must inherit xmlNode or NodeRefClass\n")
         return(NULL)
      }
      nm <- unique(sapply(x, xml_name))
      if (length(nm) > 1) {
         cat("LimsRefClass$batchcreate: all nodes must be of the same type - ", paste(nm, collapse = " "))
         return(NULL)
      }
      if (!(plural(nm[1]) %in% c("samples", "samplecreation", "containers"))){
         cat("LimsRefClass$batchcreate: only sample, and container types have batch create\n")
         return(NULL)
      }
      
      rel <- switch(plural(nm[1]),
        'samples' = 'samples',
        'containers' = 'containers',
        'samplecreation' = 'samples',
        NULL)

      rr <- lapply(split_vector(x, MAX = .self$get_max_requests(rel)),
          function(x, lims = NULL, asNode = TRUE, rel = ""){
              batch_create(x, lims, asNode = asNode, rel = rel)
          }, lims = .self, asNode = asNode, rel = rel)
        
      x <- invisible(unlist(rr))
      setname <- unname(c(samples = 'SampleSet', containers = 'ContainerSet')[rel])
      if (inherits(x, 'list')) class(x) <- append(class(x), setname)
      x
   })


#' Retrieve the max requests value by name
#' 
#' @name LimsRefClass_get_max_requests
#' @param name character the namespace to retrieve singualr or plural forms
#'  of artifacts, samples, containers or files
#' @return numeric, the max requests per batch call
NULL
LimsRefClass$methods(
    get_max_requests = function(name = 'artifacts'){
        pname <- plural(name[1])
        .self$max_requests[pname]
    })
  
#' Set the max requests value by name
#' 
#' @name LimsRefClass_set_max_requests
#' @param value numeric named vector of values to set.  Here are the defaults
#'  \itemize{
#'      \item{artifacts = 200}
#'      \item{containers = 50}
#'      \item{samples = 400}
#'      \item{files = 100}
#'  }
#' @return numeric, the max requests per batch call
NULL
LimsRefClass$methods(
    set_max_requests = function(
        values = .self$get_max_requests()
        ){
        
        allowed <- c('artifacts','containers', 'samples', 'files')
        if (!all(names(values) %in% allowed)) {
            cat("names of values must be one or more of 'artifacts','containers', 'samples' and/or 'files'\n")
            return(max_requests)
        }
        
        for (n in names(values)) .self$max_requests[n] <- values[[n]]
        invisible(.self$max_requests)
    })
      
      
      
      
#### methods above
#### functions below



#' Retrieve a batch resource
#'
#' Return order is *not* enforced to be the same as the input order
#'
#' @export
#' @param uri character vector of one or more uri
#' @param lims LimsRefClass object
#' @param rel charcater resource name
#' @param resource character resource path
#' @param asList logical, if TRUE return a named list of NodeRefClass objects
#' @param rm_dups logical, by default we remove duplicates set this to TRUE to 
#'  retrieve all, ignored if \code{asList = FALSE}
#' @param ... further arguments
#' @return a list of NodeRefClass
batch_retrieve <- function(uri, lims,
   rel = c("artifacts", "containers", "files", "samples")[1],
   resource = file.path(rel, "batch", "retrieve"), 
   asList = TRUE,
   rm_dups = TRUE, ...){
   
   if (length(uri) == 0){
        cat("batch_uri: uri has zero-length\n")
        return(list())
   }
   orig_uri <- uri
   # does the user want ALL including duplicates?
   # if so then save the IDs for later
   if (!rm_dups) limsid_all <- basename(uri)
   uri <- uri[!duplicated(uri)]

   # create new nodes for each uri requested
   linkNodes <- lapply(uri, 
      function(x, rel=rel, name = "link") {
         XML::newXMLNode(name = name, attrs = list(uri = x, rel=rel)) 
      }, rel = rel)
   
   # make the request node with uri request children
   batchNode <- XML::newXMLNode("links",
      namespace = "ri",
      namespaceDefinitions = c("ri" = "http://genologics.com/ri"),
      .children = linkNodes)
   
   URI <- file.path(lims$baseuri, resource)
   r <- httr::POST(URI, ..., body = xmlString(batchNode), 
      httr::add_headers(c("Content-Type"="application/xml")),
      handle = lims$handle,
      lims$auth)
   x <- lims$check(r)
   if (!is_exception(x) && asList){
      singleName <- switch(rel,
         "artifacts" = "artifact",
         "containers" = "container",
         "samples" = "sample",
         "files" = "file",
         rel)
      nm <- switch(singleName,
         'artifact' = 'art',
         'container' = 'con',
         'sample' = 'smp',
         'file' = 'file')
         
      nmspc <- unclass(XML::xmlNamespaces(x, simplify = TRUE))
      xx <- x[singleName]
      # transfer the the xmlnamespace to each child node
      xx <- lapply(xx, 
         function(x, nm=NULL) {
                  for (n in names(nm)) dummy <- XML::newXMLNamespace(x,nm[n])
                  x}, 
            nm = nmspc)
      if (rm_dups == FALSE) {
         # name each node
         names(xx) <- sapply(xx, function(x) xml_atts(x)["limsid"])
         # rebuild the list with duplicates
         xx <- xx[limsid_all]
      }
      
      # make sure we have the original order
      #names(xx) <- trimuri(sapply(xx, function(x) xml_atts(x)['uri']))
      #xx <- xx[orig_uri]
      #names(xx) <- sapply(xx, function(x) xml_atts(x)["limsid"])
      
   } else {
      xx <- x
   }
   invisible(xx) 
} # batch_retrieve


#' Update one or more XML::xmlNodes using batch resources
#'
#' Return order is *not* enforced to be the same as the input order
#'
#' @export
#' @param x a list of one or more XML::xmlNode objects
#' @param lims LimsRefClass object
#' @param asNode logical, if TRUE return a named list of NodeRefClass objects
#' @param rel the relative namespace into the "batch/retrieve"
#' @return list of XML::xmlNode or NodeRefClass
batch_update <- function(x, lims, asNode = TRUE,
    rel = c("artifacts", "containers", "samples" )[1]){
    
    stopifnot(all(sapply(x, function(x) inherits(x,'XMLAbstractNode')))) 
        
    detail <- switch(rel,
         'artifacts' = create_artifacts_details(x),
         'containers' = create_containers_details(x),
         'samples' = create_samples_details(x),
         NULL)
      
    if (is.null(detail)){ 
        cat("batch_update: only artifact, sample and container types have batch update\n")
        return(NULL)
    }
    orig_uri <- sapply(x, function(x) xml_atts(x)[['uri']])
    
    URI <- lims$uri(paste0(rel, "/batch/update"))
    r <- httr::POST(URI, body = xmlString(detail), 
         httr::add_headers(c("Content-Type"="application/xml")),
         handle = lims$handle,
         lims$auth)
    x <- lims$check(r)
    if (!is_exception(x)) {
       #uri <- sapply(x['link'], function(x) xml_atts(x)[['uri']])
       r <- batch_retrieve(orig_uri, lims ,rel = rel)
       if (asNode) {
          r <- lapply(r, parse_node, lims)
          # since we have sample, artifact or container we know we can have 
          # a name
          names(r) <- sapply(r, function(x) x$name)
       }
    } else {
       r <- NULL
    }
    
    invisible(r)
}


#' Create one or more nodes (Sample and Container only)
#' 
#' Return order is *not* enforced to be the same as the input order
#'
#' @export
#' @param x a list of one or more XML::xmlNode
#' @param lims a LimsRefClass node 
#' @param asNode logical, if TRUE return a named list of NodeRefClass objects
#' @param rel the relative namespace into the "batch/create"
#' @return list of XML::xmlNode or NodeRefClass
batch_create <- function(x, lims, asNode = asNode, 
     rel = c("samples", "containers")[1]){
    
     rel <- plural(rel)
     detail <- switch(rel,
         'containers' = create_containers_details(x),
         "samples" = create_samples_details(x),
         "samplecreation" = create_samples_details(x),
         NULL)
      if (is.null(detail)){
          cat("batch_create: only sample and container types have batch create\n")
          return(NULL)
      }
      
      real_rel <- switch(rel,
         "samplecreation" = "samples",
         rel)
            
     
      URI <- lims$uri(file.path(real_rel, "batch", "create"))
      r <- httr::POST(url=URI, body = xmlString(detail), 
         httr::content_type_xml(),
         lims$auth)
      
      x <- lims$check(r)
      if (!is_exception(x)) {
         uri <- sapply(x['link'], function(x) xml_atts(x)[['uri']])
         r <- batch_retrieve(uri, lims, , rel = rel)
         if (asNode) r <- lapply(r, parse_node, lims)
      } else {
         print(x)
         r <- NULL
      }
      
      invisible(r)    
}


#' Get a uri with option to retry up to \code{tries} times.  Useful when doing
#' vulnerable batch operations but could be used anytime.
#'
#' @export
#' @param uri character, the uri to retrieve
#' @param lims a LimsRefClass node
#' @param ... further arguments for httr::GET()
#' @param tries numeric, allow up to this number of tries before failing
#' @return result of httr::GET
try_GET <- function(uri, lims, ..., tries = 3){
    i <- 1
    while(i <= tries){
        x <- httr::GET(uri,  
            ...,
            encoding = lims$encoding,
            handle = lims$handle,
            lims$auth)
        if (!is_exception(x)) break;
        Sys.sleep(1) # just chill
        i <- i + 1
    }
    x
}
    
    
#' List URIs in a resource such as samples or containers.
#'
#' @export
#' @param lims the LimsRefClass object to query
#' @param resource character the uri to get
#' @param n numeric, the maximum number of URI, NA to get all
#' @param ... further arguments for httr::GET including \code{query} list
#' @return character vector of zero or more URI
#' @examples
#' \dontrun{
#'     # list the samples in a project
#'     ss <- list_resources(lims,'samples', projectname = 'foobar')
#' }
list_resource <- function(lims, resource, n = NA, ...){
    
      extract_uri <- function(x) { xml_atts(x)[['uri']] }
      
      N <- if(is.na(n)) 10e6 else n[1]
      
      presource <- genologicsr::plural(resource)
      sresource <- genologicsr::singular(resource)
      
      qry <- genologicsr::build_query(list(...))
      x <- lims$GET(lims$uri(presource), depaginate = FALSE, asNode = FALSE, query = qry) 
      r <- x[sresource]
      if (is.null(r)) return("")
      
      while(length(r) < N) {
        np <- x[['next-page']]
        if (is.null(np)) break
        np_uri <- xml_atts(np)[['uri']]
        x <- lims$GET(np_uri, depaginate = FALSE, asNode = FALSE)
        r <- c(r, x[sresource])
        if (length(r) >= N) break
      }
      
      N <- if(is.na(n)) length(r) else n
      r <- sapply(r[1:N], extract_uri)
      names(r) <- basename(r)
      invisible(r)
}

#' Get a uri with option to depaginate
#'
#' @export
#' @param uri character, the uri to retrieve
#' @param lims a LimsRefClass node
#' @param ... further arguments for httr::GET()
#' @param depaginate logical, if TRUE (the default) then depaginate the results
#' @param verbose logical if TRUE be verbose
#' @param tries numeric, allow up to this number of tries before failing
#' @return XML::xmlNode
get_uri <- function(uri, lims, ..., depaginate = TRUE, verbose = FALSE, tries = 3){

      if (verbose) cat("get_uri:", uri, "\n")

      # since when has LIMS substituted "+" for spaces ("%20")?
      # @param uri
      # @return updated param
      no_plus_uri <- function(x){
         file.path(dirname(x), gsub("+", "%20", basename(x), fixed = TRUE))
      }
      
      uri <- no_plus_uri(uri)
      # first pass
      x <- httr::GET(uri,  
         ...,
         encoding = lims$encoding,
         handle = lims$handle,
         lims$auth)
      #x <- try_GET(uri, lims, ...)

      x <- lims$check(x) 
      if ( !is_exception(x) && ("next-page" %in% names(x))  && depaginate ){
         yuri <- no_plus_uri(xml_atts(x[['next-page']])[['uri']])
         doNext <- TRUE
         while(doNext){
            y <- lims$check(httr::GET(yuri, encoding = lims$encoding,
               handle = lims$handle, lims$auth))
            #y <- lims$check(try_GET(yuri,lims))
            children <- !(names(y) %in% c("previous-page", "next-page"))
            if (any(children)) x <- XML::addChildren(x, kids = y[children])
            doNext <- "next-page" %in% names(y)
            if (doNext) yuri <- no_plus_uri(xml_atts(y[["next-page"]])[["uri"]])
         } # doNext while loop
         x <- XML::removeChildren(x, kids = x["next-page"])
      }
   invisible(x)
}


#' Convert a node to an object inheriting from NodeRefClass 
#'
#' @family Lims Node
#' @export
#' @param node XML::xmlNode
#' @param lims LimsRefClass object
#' @return an object the inherits from NodeRefClass
parse_node <- function(node, lims){

   if (!is_xmlNode(node)) stop("parse_node: node must be XML::xmlNode")
   if (!inherits(lims, 'LimsRefClass')) stop("assign_node: lims must be LimsRefClass")
   
   nm <- xml_name(node)[1]
   switch(nm,
       'artifact' = ArtifactRefClass$new(node, lims),
       'processes' = ProcessRefClass$new(node, lims),
       'process' = ProcessRefClass$new(node, lims),
       'container' = ContainerRefClass$new(node, lims),
       'sample' = SampleRefClass$new(node,lims),
       'input-output-map' = InputOutputMapRefClass$new(node, lims),
       'researcher' = ResearcherRefClass$new(node, lims),
       'file' = FileRefClass$new(node, lims),
       'field' = FieldRefClass$new(node, lims),
       'project' = ProjectRefClass$new(node, lims),
       'projects' = ProjectsRefClass$new(node, lims),
       'container-type' = ContainerTypeRefClass$new(node, lims),
       'instrument' = InstrumentRefClass$new(node, lims),
       'process-type' = ProcessTypeRefClass$new(node, lims),
       'exception' = ExceptionRefClass$new(node, lims),
       'artifactgroup' = ArtifactGroupRefClass$new(node, lims),
       'lab' = LabRefClass(node, lims),
       'workflow' = WorkflowRefClass(node, lims),
       'step' = StepRefClass(node, lims),
       'stage' = StageRefClass(node, lims),
       'protocol' = ProtocolRefClass(node, lims),
       NodeRefClass$new(node, lims))

}

#' Instantiate a LimsRefClass object
#'
#' @export
#' @param configfile character, the fully qualified path to the config file
#' @return a LimsRefClass instance or NULL
Lims <- function(configfile = build_config_path()){
   if (!file.exists(configfile[1])) stop("configfile not found:", configfile[1])
   x <- try(read_config(configfile[1]))
   if (inherits(x, "try-error")) stop("Error reading config file")
   
   X <- LimsRefClass$new()
   X$field("encoding", "UTF-8")
   X$field("version", get_config(x, "genologics", "VERSION", default = ""))
   buri <- get_config(x, "genologics", "BASEURI", default = "")
   if (nchar(buri) == 0) stop("base uri not found in config file")
   X$field("baseuri", file.path(buri, "api", X$version))
   X$field("handle", httr::handle(buri))
   X$field('auth', 
      httr::authenticate(get_config(x, "genologics", "USERNAME", default = ""),
                   get_config(x, "genologics", "PASSWORD", default = "") 
      ) )
   X$field('fileauth',
      httr::authenticate(get_config(x, "glsfilestore", "USERNAME", default = ""),
                   get_config(x, "glsfilestore", "PASSWORD", default = "") 
      ) )   
   if (!is.null(x[['max_requests']])){
      b <- X$max_requests
      for (n in names(b)) b[n] <- as.numeric(get_config(x, "max_requests", n, default = b[n]))
      X$set_max_requests(b)
   }
   if (!X$validate_session()) {
      warning("API session failed validation")
   } 
   X
}
BigelowLab/genologicsr documentation built on May 5, 2019, 2:42 p.m.