blob: 967feaa7bc4121ffc304c0c1f665e3c597f46323 [file] [log] [blame]
Marc Kupietzfd9e7492019-11-08 15:45:18 +01001################################################################################
2# Use setClassUnion to define the unholy NULL-data union as a virtual class.
3################################################################################
4setClassUnion("characterOrNULL", c("character", "NULL"))
Marc Kupietza4675722022-02-23 23:55:15 +01005setClassUnion("listOrNULL", c("list", "NULL"))
Marc Kupietzf83d59a2025-02-01 14:48:30 +01006# setOldClass("httr2_oauth_client")
Marc Kupietzfd9e7492019-11-08 15:45:18 +01007
Marc Kupietz6a16d7d2026-08-31 21:36:01 +02008#' @rdname KorAPConnection
9#' @export
10setClass("KorAPConnection", slots = c(KorAPUrl = "character", apiVersion = "character", indexRevision = "characterOrNULL", apiUrl = "character", accessToken = "characterOrNULL", oauthClient = "ANY", oauthScope = "characterOrNULL", authorizationSupported = "logical", userAgent = "character", timeout = "numeric", verbose = "logical", cache = "logical", welcome = "listOrNULL"))
11
12generic_kor_app_id <- "99FbPHH7RrN36hbndF7b6f"
13
14kustvakt_redirect_uri <- "http://localhost:1410/"
15kustvakt_auth_path <- "settings/oauth/authorize"
16
17#' Default KorAP server URL
18#'
19#' Returns the KorAP instance URL to connect to if none is given explicitly:
20#' the environment variable `KORAP_URL` if it is set and non-empty, and the
21#' IDS Mannheim KorAP main instance otherwise.
22#'
23#' @return URL of the KorAP instance to connect to by default
24#' @keywords internal
25defaultKorAPUrl <- function() {
26 url <- Sys.getenv("KORAP_URL", unset = "")
27 if (nzchar(url)) url else "https://korap.ids-mannheim.de/"
28}
29
Marc Kupietza8c40f42025-06-24 15:49:52 +020030#' Connect to KorAP Server
Marc Kupietz25aebc32019-09-16 18:40:50 +020031#'
Marc Kupietza8c40f42025-06-24 15:49:52 +020032#' `KorAPConnection()` creates a connection to a KorAP server for corpus queries.
33#' This is your starting point for all corpus analysis tasks.
Marc Kupietze95108e2019-09-18 13:23:58 +020034#'
Marc Kupietza8c40f42025-06-24 15:49:52 +020035#' Use `KorAPConnection()` to connect, then `corpusQuery()` to search, and
36#' `fetchAll()` to retrieve results. For authorized access to restricted corpora,
37#' use `auth()` or provide an `accessToken`.
38#'
39#' @section Basic Workflow:
40#' ```r
41#' # Connect to KorAP
42#' kcon <- KorAPConnection()
43#'
44#' # Search for a term
45#' query <- corpusQuery(kcon, "Ameisenplage")
46#'
47#' # Get all results
48#' results <- fetchAll(query)
49#' ```
50#'
51#' @section Authorization:
52#' For access to restricted corpora, authorize your connection:
53#' ```r
54#' kcon <- KorAPConnection() |> auth()
55#' ```
56#'
Marc Kupietzf9914bb2025-06-25 09:57:55 +020057#' @param KorAPUrl URL of the web user interface of the KorAP server instance you want to access.
58#' Defaults to the environment variable `KORAP_URL` if set and to the IDS Mannheim KorAP main instance
Marc Kupietz36117de2025-06-25 12:46:10 +020059#' to query DeReKo, otherwise. In order to access the KorAP instance at the German
60#' National Library (DNB) to query the contemporary fiction corpus DeLiKo@@DNB,
61#' for example, set `KorAPUrl` to <https://korap.dnb.de/>.
Marc Kupietzf9914bb2025-06-25 09:57:55 +020062#' @param apiVersion which version of KorAP's API you want to connect to. Defaults to "v1.0".
63#' @param apiUrl URL of the KorAP web service. If not provided, it will be constructed from KorAPUrl and apiVersion.
64#' @param accessToken OAuth2 access token. For queries on corpus parts with restricted
65#' access (e.g. textual queries on IPR protected data), you need to authorize
66#' your application with an access token.
67#' You can obtain an access token in the OAuth settings of your KorAP web interface.
68#'
69#' More details are explained in the
70#' [authorization section](https://github.com/KorAP/RKorAPClient#authorization)
71#' of the RKorAPClient Readme on GitHub.
72#'
73#' To use authorization based on an access token
74#' in subsequent queries, initialize your KorAP connection with:
75#'
76#' ```
77#' kco <- KorAPConnection(accessToken="<access token>")
78#' ```
79#'
80#' In order to make the API
81#' token persistent for the currently used `KorAPUrl` (you can have one
82#' token per KorAPUrl / KorAP server instance), use:
83#'
84#' ```
85#' persistAccessToken(kco)
86#' ```
87#'
88#' This will store it in your keyring using the
89#' [keyring::keyring-package]. Subsequent KorAPConnection() calls will
90#' then automatically retrieve the token from your keying. To stop using a
91#' persisted token, call `clearAccessToken(kco)`. Please note that for
92#' DeReKo, authorized queries will behave differently inside and outside the
93#' IDS, because of the special license situation. This concerns also cached
94#' results which do not take into account from where a request was issued. If
95#' you experience problems or unexpected results, please try `kco <-
96#' KorAPConnection(cache=FALSE)` or use
97#' [clearCache()] to clear the cache completely.
98#'
99#' An alternative to using an access token is to use a browser-based oauth2 workflow
100#' to obtain an access token. This can be done with the [auth()] method.
Marc Kupietz36117de2025-06-25 12:46:10 +0200101#' @param oauthClient OAuth2 client object.
Marc Kupietzf9914bb2025-06-25 09:57:55 +0200102#' @param oauthScope OAuth2 scope. Defaults to "search match_info".
103#' @param authorizationSupported logical that indicates if authorization is supported/necessary for the current KorAP instance. Automatically set during initialization.
104#' @param userAgent user agent string. Defaults to "R-KorAP-Client".
105#' @param timeout timeout in seconds for API requests (this does not influence server internal timeouts). Defaults to 240 seconds.
106#' @param verbose logical that decides whether following operations will default to
Marc Kupietz39da9dc2025-09-10 13:54:40 +0200107#' be verbose. Defaults to FALSE. If not explicitly provided, this can be overridden
108#' via environment variable `KORAP_VERBOSE` (accepted true-ish values: 1, true, yes, on)
109#' or R option `rkorap.verbose` (logical).
Marc Kupietzf9914bb2025-06-25 09:57:55 +0200110#' @param cache logical that decides if API calls are cached locally. You can clear
111#' the cache with [clearCache()]. Defaults to TRUE.
112#'
113#' @return [KorAPConnection()] object that can be used e.g. with [corpusQuery()]
114#'
Marc Kupietza8c40f42025-06-24 15:49:52 +0200115#' @details
116#' The KorAPConnection object contains various configuration slots for advanced users:
117#' KorAPUrl (server URL), apiVersion, accessToken (OAuth2 token),
118#' timeout (request timeout), verbose (logging), cache (local caching),
119#' and other technical parameters. Most users can ignore these implementation details.
120#'
121#' @family initialization functions
Marc Kupietz0a96b282019-10-01 11:05:31 +0200122#' @import R.cache
Marc Kupietze95108e2019-09-18 13:23:58 +0200123#' @import utils
124#' @import methods
Marc Kupietz6dfeed92025-06-03 11:58:06 +0200125#' @include logging.R
Marc Kupietze95108e2019-09-18 13:23:58 +0200126#' @export
Marc Kupietz6a16d7d2026-08-31 21:36:01 +0200127KorAPConnection <- function(KorAPUrl = defaultKorAPUrl(),
128 apiVersion = "v1.0",
129 apiUrl,
130 accessToken = getAccessToken(KorAPUrl),
131 oauthClient = NULL,
132 oauthScope = "search match_info",
133 authorizationSupported = TRUE,
134 userAgent = "R-KorAP-Client",
135 timeout = 240,
136 verbose = FALSE,
137 cache = TRUE) {
138 # Forward only the arguments that were actually supplied, so that the
139 # defaults and the `missing()` based overrides of the initialize method
140 # (see below) keep working. The defaults above merely mirror those of the
141 # initialize method to document them (see test-korapconnection-signature.R).
142 args <- as.list(match.call())[-1L]
143 do.call(methods::new, c("KorAPConnection", args), envir = parent.frame())
144}
Marc Kupietze95108e2019-09-18 13:23:58 +0200145
Marc Kupietza8c40f42025-06-24 15:49:52 +0200146#' Initialize KorAPConnection object
147#' @keywords internal
Marc Kupietz632cbd42019-09-06 16:04:51 +0200148#' @export
Marc Kupietz36117de2025-06-25 12:46:10 +0200149#'
Marc Kupietzb79fd442025-03-26 10:25:03 +0100150setMethod("initialize", "KorAPConnection", function(.Object,
Marc Kupietz6a16d7d2026-08-31 21:36:01 +0200151 KorAPUrl = defaultKorAPUrl(),
Marc Kupietza824d502025-05-02 15:40:23 +0200152 apiVersion = "v1.0",
Marc Kupietzb79fd442025-03-26 10:25:03 +0100153 apiUrl,
154 accessToken = getAccessToken(KorAPUrl),
155 oauthClient = NULL,
156 oauthScope = "search match_info",
157 authorizationSupported = TRUE,
158 userAgent = "R-KorAP-Client",
159 timeout = 240,
160 verbose = FALSE,
161 cache = TRUE) {
162 .Object <- callNextMethod()
163 m <- regexpr("https?://[^?]+", KorAPUrl, perl = TRUE)
164 .Object@KorAPUrl <- regmatches(KorAPUrl, m)
Marc Kupietza824d502025-05-02 15:40:23 +0200165 if (!endsWith(.Object@KorAPUrl, "/")) {
Marc Kupietzb79fd442025-03-26 10:25:03 +0100166 .Object@KorAPUrl <- paste0(.Object@KorAPUrl, "/")
167 }
168 if (missing(apiUrl)) {
Marc Kupietza824d502025-05-02 15:40:23 +0200169 .Object@apiUrl <- paste0(.Object@KorAPUrl, "api/", apiVersion, "/")
170 } else {
171 .Object@apiUrl <- apiUrl
172 }
173 .Object@accessToken <- accessToken
174 .Object@oauthClient <- oauthClient
175 .Object@apiVersion <- apiVersion
176 .Object@userAgent <- userAgent
177 .Object@oauthScope <- oauthScope
178 .Object@authorizationSupported <- authorizationSupported
179 .Object@timeout <- timeout
Marc Kupietz39da9dc2025-09-10 13:54:40 +0200180 # Allow environment/option override only if user did not pass `verbose`
181 if (missing(verbose)) {
182 ev <- Sys.getenv("KORAP_VERBOSE", unset = "")
183 if (nzchar(ev)) {
184 verbose <- tolower(ev) %in% c("1", "true", "t", "yes", "y", "on")
185 } else {
186 opt <- getOption("rkorap.verbose", NULL)
187 if (!is.null(opt)) verbose <- isTRUE(opt)
188 }
189 }
Marc Kupietza824d502025-05-02 15:40:23 +0200190 .Object@verbose <- verbose
191 .Object@cache <- cache
192 .Object@welcome <- apiCall(.Object, .Object@apiUrl, json = FALSE, cache = FALSE, getHeaders = TRUE)
193 if (!is.null(.Object@welcome)) {
194 message(.Object@welcome[[2]])
195 resp <- httr2::request(.Object@KorAPUrl) |>
196 httr2::req_url_path_append(kustvakt_auth_path) |>
197 httr2::req_error(is_error = \(resp) FALSE) |>
198 httr2::req_perform()
199 .Object@authorizationSupported <- (httr2::resp_status(resp) == 200)
Marc Kupietz62b17892025-02-01 18:26:45 +0100200
Marc Kupietza824d502025-05-02 15:40:23 +0200201 .Object@indexRevision <- .Object@welcome[[1]][["x-index-revision"]]
202 } else {
203 if (grepl(.Object@KorAPUrl, .Object@apiUrl)) {
204 message("Could not connect to KorAP instance ", .Object@KorAPUrl)
205 } else {
206 message("Could not connect to KorAP API at ", .Object@apiUrl)
207 }
208 }
209 .Object
210})
Marc Kupietze95108e2019-09-18 13:23:58 +0200211
Marc Kupietza96537f2019-11-09 23:07:44 +0100212
Marc Kupietzb956b812019-11-25 17:53:13 +0100213accessTokenServiceName <- "RKorAPClientAccessToken"
Marc Kupietz4862b862019-11-07 10:13:53 +0100214
Marc Kupietza824d502025-05-02 15:40:23 +0200215setGeneric("persistAccessToken", function(kco, ...) standardGeneric("persistAccessToken"))
Marc Kupietz4862b862019-11-07 10:13:53 +0100216
Marc Kupietza4f51d72025-01-25 16:23:18 +0100217#' Persist current access token in keyring
218#'
Marc Kupietza8c40f42025-06-24 15:49:52 +0200219#' @family initialization functions
Marc Kupietza4f51d72025-01-25 16:23:18 +0100220#' @param kco KorAPConnection object
221#' @param accessToken access token to be persisted. If not supplied, the current access token of the KorAPConnection object will be used.
222#' @return KorAPConnection object.
223#'
Marc Kupietzb956b812019-11-25 17:53:13 +0100224#' @aliases persistAccessToken
Marc Kupietza4f51d72025-01-25 16:23:18 +0100225#'
Marc Kupietz4862b862019-11-07 10:13:53 +0100226#' @import keyring
227#' @export
Marc Kupietza4f51d72025-01-25 16:23:18 +0100228#'
Marc Kupietz4862b862019-11-07 10:13:53 +0100229#' @examples
230#' \dontrun{
Marc Kupietza824d502025-05-02 15:40:23 +0200231#' kco <- KorAPConnection(accessToken = "e739u6eOzkwADQPdVChxFg")
Marc Kupietzb956b812019-11-25 17:53:13 +0100232#' persistAccessToken(kco)
Marc Kupietza4f51d72025-01-25 16:23:18 +0100233#'
Marc Kupietza824d502025-05-02 15:40:23 +0200234#' kco <- KorAPConnection() %>%
235#' auth(app_id = "<my application id>") %>%
236#' persistAccessToken()
Marc Kupietz4862b862019-11-07 10:13:53 +0100237#' }
238#'
Marc Kupietza4f51d72025-01-25 16:23:18 +0100239#' @seealso [clearAccessToken()], [auth()]
240#'
Marc Kupietza824d502025-05-02 15:40:23 +0200241setMethod("persistAccessToken", "KorAPConnection", function(kco, accessToken = kco@accessToken) {
242 if (!is.null(kco@oauthClient)) {
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100243 warning("Short lived access tokens from a confidential application cannot be persisted.")
244 return(kco)
245 }
Marc Kupietza824d502025-05-02 15:40:23 +0200246 if (is.null(accessToken)) {
Marc Kupietzb956b812019-11-25 17:53:13 +0100247 stop("It seems that you have not supplied any access token that could be persisted.", call. = FALSE)
Marc Kupietza824d502025-05-02 15:40:23 +0200248 }
Marc Kupietz4862b862019-11-07 10:13:53 +0100249
Marc Kupietzb956b812019-11-25 17:53:13 +0100250 kco@accessToken <- accessToken
251 key_set_with_value(accessTokenServiceName, kco@KorAPUrl, accessToken)
Marc Kupietza4f51d72025-01-25 16:23:18 +0100252 return(kco)
Marc Kupietz4862b862019-11-07 10:13:53 +0100253})
254
Marc Kupietza824d502025-05-02 15:40:23 +0200255setGeneric("clearAccessToken", function(kco) standardGeneric("clearAccessToken"))
Marc Kupietz4862b862019-11-07 10:13:53 +0100256
Marc Kupietza4f51d72025-01-25 16:23:18 +0100257#' Clear access token from keyring and KorAPConnection object
258#'
Marc Kupietza8c40f42025-06-24 15:49:52 +0200259#' @family initialization functions
Marc Kupietzb956b812019-11-25 17:53:13 +0100260#' @aliases clearAccessToken
Marc Kupietz4862b862019-11-07 10:13:53 +0100261#' @import keyring
Marc Kupietza4f51d72025-01-25 16:23:18 +0100262#' @param kco KorAPConnection object
263#' @return KorAPConnection object with access token set to `NULL`.
Marc Kupietz4862b862019-11-07 10:13:53 +0100264#' @export
265#' @examples
Marc Kupietza4f51d72025-01-25 16:23:18 +0100266#' \dontrun{
Marc Kupietz617266d2025-02-27 10:43:07 +0100267#' kco <- KorAPConnection()
Marc Kupietza4f51d72025-01-25 16:23:18 +0100268#' kco <- clearAccessToken(kco)
Marc Kupietz4862b862019-11-07 10:13:53 +0100269#' }
270#'
Marc Kupietza4f51d72025-01-25 16:23:18 +0100271#' @seealso [persistAccessToken()]
272#'
Marc Kupietza824d502025-05-02 15:40:23 +0200273setMethod("clearAccessToken", "KorAPConnection", function(kco) {
Marc Kupietzb956b812019-11-25 17:53:13 +0100274 key_delete(accessTokenServiceName, kco@KorAPUrl)
Marc Kupietza4f51d72025-01-25 16:23:18 +0100275 kco@accessToken <- NULL
276 kco
Marc Kupietz4862b862019-11-07 10:13:53 +0100277})
278
Marc Kupietza4f51d72025-01-25 16:23:18 +0100279
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100280oauthRefresh <- function(req, client, scope, kco) {
Marc Kupietza824d502025-05-02 15:40:23 +0200281 httr2::req_oauth_auth_code(req, client,
282 scope = scope,
283 auth_url = paste0(kco@KorAPUrl, kustvakt_auth_path),
284 redirect_uri = kustvakt_redirect_uri,
285 cache_key = kco@KorAPUrl
286 )
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100287}
288
Marc Kupietza824d502025-05-02 15:40:23 +0200289setGeneric("auth", function(kco, app_id = generic_kor_app_id, app_secret = NULL, scope = kco@oauthScope) standardGeneric("auth"))
Marc Kupietza4f51d72025-01-25 16:23:18 +0100290
291#' Authorize RKorAPClient
292#'
Marc Kupietza8c40f42025-06-24 15:49:52 +0200293#' @family initialization functions
Marc Kupietza4f51d72025-01-25 16:23:18 +0100294#' @aliases auth
295#'
296#' @description
Marc Kupietza4f51d72025-01-25 16:23:18 +0100297#'
298#' Authorize RKorAPClient to make KorAP queries and download results on behalf of the user.
299#'
300#' @param kco KorAPConnection object
301#' @param app_id OAuth2 application id. Defaults to the generic KorAP client application id.
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100302#' @param app_secret OAuth2 application secret. Used with confidential client applications. Defaults to `NULL`.
Marc Kupietza4f51d72025-01-25 16:23:18 +0100303#' @param scope OAuth2 scope. Defaults to "search match_info".
304#' @return KorAPConnection object with access token set in `@accessToken`.
305#'
306#' @importFrom httr2 oauth_client oauth_flow_auth_code
307#' @examples
308#' \dontrun{
Marc Kupietz617266d2025-02-27 10:43:07 +0100309#' kco <- KorAPConnection(verbose = TRUE) %>% auth()
Marc Kupietza5501652025-01-28 20:25:42 +0100310#' df <- collocationAnalysis(kco, "focus([marmot/p=ADJA] {Ameisenplage})",
Marc Kupietza824d502025-05-02 15:40:23 +0200311#' leftContextSize = 1, rightContextSize = 0
312#' )
Marc Kupietza4f51d72025-01-25 16:23:18 +0100313#' }
314#'
315#' @seealso [persistAccessToken()], [clearAccessToken()]
316#'
317#' @export
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100318setMethod("auth", "KorAPConnection", function(kco, app_id = generic_kor_app_id, app_secret = NULL, scope = kco@oauthScope) {
Marc Kupietz62b17892025-02-01 18:26:45 +0100319 if (kco@authorizationSupported == FALSE) {
320 log_info(kco@verbose, "Authorization is not supported by this KorAP instance.")
321 return(kco)
322 }
Marc Kupietza824d502025-05-02 15:40:23 +0200323 if (kco@KorAPUrl != "https://korap.ids-mannheim.de/" & app_id == generic_kor_app_id) {
Marc Kupietza4f51d72025-01-25 16:23:18 +0100324 warning(paste("You can use the default app_id only for the IDS Mannheim KorAP main instance for querying DeReKo. Please provide your own app_id for accesing", kco@KorAPUrl))
325 return(kco)
326 }
327 if (is.null(kco@accessToken) || is.null(kco@welcome)) { # if access token is not set or invalid
Marc Kupietza824d502025-05-02 15:40:23 +0200328 client <- if (!is.null(kco@oauthClient)) {
329 kco@oauthClient
330 } else {
Marc Kupietza4f51d72025-01-25 16:23:18 +0100331 httr2::oauth_client(
Marc Kupietza824d502025-05-02 15:40:23 +0200332 id = app_id,
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100333 secret = app_secret,
Marc Kupietza4f51d72025-01-25 16:23:18 +0100334 token_url = paste0(kco@apiUrl, "oauth2/token")
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100335 )
Marc Kupietza824d502025-05-02 15:40:23 +0200336 }
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100337 if (is.null(app_secret)) {
Marc Kupietza824d502025-05-02 15:40:23 +0200338 kco@accessToken <- (client |>
Marc Kupietza4f51d72025-01-25 16:23:18 +0100339 httr2::oauth_flow_auth_code(
340 scope = scope,
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100341 auth_url = paste0(kco@KorAPUrl, kustvakt_auth_path),
Marc Kupietz62b17892025-02-01 18:26:45 +0100342 redirect_uri = kustvakt_redirect_uri
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100343 ))$access_token
344 log_info(kco@verbose, "Client authorized. New access token set.")
345 } else {
346 kco@oauthClient <- client
347 kco@oauthScope <- scope
348 req <- request(kco@apiUrl) |>
349 oauthRefresh(client, scope, kco) |>
350 req_perform()
351 log_info(kco@verbose, "Client authorized. Short lived access token will be refreshed automatically.")
352 }
Marc Kupietza4f51d72025-01-25 16:23:18 +0100353 } else {
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100354 log_info(kco@verbose, "Access token already set.")
Marc Kupietza4f51d72025-01-25 16:23:18 +0100355 }
356 return(kco)
357})
358
359
360
Marc Kupietz4862b862019-11-07 10:13:53 +0100361#' @import keyring
Marc Kupietzb956b812019-11-25 17:53:13 +0100362getAccessToken <- function(KorAPUrl) {
Marc Kupietza824d502025-05-02 15:40:23 +0200363 keyList <- tryCatch(
364 withCallingHandlers(key_list(service = accessTokenServiceName),
365 warning = function(w) invokeRestart("muffleWarning"),
366 error = function(e) {
367 return(NULL)
368 }
369 ),
370 error = function(e) { }
371 )
372 if (KorAPUrl %in% keyList$username) {
Marc Kupietzb956b812019-11-25 17:53:13 +0100373 key_get(accessTokenServiceName, KorAPUrl)
Marc Kupietza824d502025-05-02 15:40:23 +0200374 } else {
Marc Kupietzfd9e7492019-11-08 15:45:18 +0100375 NULL
Marc Kupietza824d502025-05-02 15:40:23 +0200376 }
Marc Kupietz4862b862019-11-07 10:13:53 +0100377}
Marc Kupietz0a96b282019-10-01 11:05:31 +0200378
Marc Kupietz581a29b2021-09-04 20:51:04 +0200379
Marc Kupietz62b17892025-02-01 18:26:45 +0100380warnIfNotAuthorized <- function(kco) {
381 if (kco@authorizationSupported & is.null(kco@accessToken) & is.null(kco@oauthClient)) {
Marc Kupietz581a29b2021-09-04 20:51:04 +0200382 warning(
383 paste0(
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100384 "In order to receive KWICSs also from corpora with restricted licenses, you may need to\n",
385 "authorize your application with an access token or the auth() method.\n",
386 "To generate an access token, login to KorAP and navigate to KorAP's OAuth settings <",
Marc Kupietz581a29b2021-09-04 20:51:04 +0200387 kco@KorAPUrl,
388 "settings/oauth#page-top>"
389 )
390 )
391 }
392}
393
Marc Kupietz0a96b282019-10-01 11:05:31 +0200394KorAPCacheSubDir <- function() {
Marc Kupietza824d502025-05-02 15:40:23 +0200395 paste0(
396 "RKorAPClient_",
397 gsub(
398 "^([0-9]+\\.[0-9]+).*",
399 "\\1",
400 packageVersion("RKorAPClient"),
401 perl = TRUE
402 )
403 )
Marc Kupietz0a96b282019-10-01 11:05:31 +0200404}
405
Marc Kupietza824d502025-05-02 15:40:23 +0200406setGeneric("apiCall", function(kco, ...) standardGeneric("apiCall"))
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200407
Marc Kupietz4de53ec2019-10-04 09:12:00 +0200408## quiets concerns of R CMD check re: the .'s that appear in pipelines
Marc Kupietzef1ef4a2025-02-19 12:12:40 +0100409utils::globalVariables(c("."))
Marc Kupietz4de53ec2019-10-04 09:12:00 +0200410
Marc Kupietza8c40f42025-06-24 15:49:52 +0200411#' Internal API call method
412#' @keywords internal
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200413#' @aliases apiCall
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200414#' @param kco KorAPConnection object
415#' @param url request url
Marc Kupietzf9129592025-01-26 19:17:54 +0100416#' @param json logical that determines if JSON result is expected
Marc Kupietzb49afa02020-06-04 15:50:29 +0200417#' @param getHeaders logical that determines if headers and content should be returned (as a list)
Marc Kupietz69cc54a2019-09-30 12:06:54 +0200418#' @importFrom jsonlite fromJSON
Marc Kupietza4675722022-02-23 23:55:15 +0100419#' @importFrom curl has_internet
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100420#' @import httr2
Marc Kupietz69cc54a2019-09-30 12:06:54 +0200421#' @export
Marc Kupietzf9129592025-01-26 19:17:54 +0100422setMethod("apiCall", "KorAPConnection", function(kco, url, json = TRUE, getHeaders = FALSE, cache = kco@cache, timeout = kco@timeout) {
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100423 result <- ""
Marc Kupietzf9129592025-01-26 19:17:54 +0100424
425 # Handle caching if enabled
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100426 if (cache) {
Marc Kupietzf9129592025-01-26 19:17:54 +0100427 result <- R.cache::loadCache(dir = KorAPCacheSubDir(), key = list(url, kco@accessToken, kco@indexRevision))
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100428 if (!is.null(result)) {
Marc Kupietzf9129592025-01-26 19:17:54 +0100429 if (!is.null(result$meta)) result$meta$cached <- "local"
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100430 return(result)
Marc Kupietz0a96b282019-10-01 11:05:31 +0200431 }
432 }
Marc Kupietza4675722022-02-23 23:55:15 +0100433
Marc Kupietzf9129592025-01-26 19:17:54 +0100434 # Check for internet connection
Marc Kupietza4675722022-02-23 23:55:15 +0100435 if (!curl::has_internet()) {
436 message("No internet connection.")
437 return(invisible(NULL))
438 }
439
Marc Kupietzf9129592025-01-26 19:17:54 +0100440 # Create the request
441 req <- httr2::request(url) |>
442 httr2::req_user_agent(kco@userAgent) |>
443 httr2::req_timeout(timeout)
Marc Kupietza4675722022-02-23 23:55:15 +0100444
Marc Kupietz03402e72025-05-02 15:39:40 +0200445 if (!is.null(kco@oauthClient)) {
446 req <- req |> oauthRefresh(kco@oauthClient, scope = kco@oauthScope, kco)
Marc Kupietzf83d59a2025-02-01 14:48:30 +0100447 } else if (!is.null(kco@accessToken)) {
448 req <- req |> httr2::req_auth_bearer_token(kco@accessToken)
Marc Kupietzf9129592025-01-26 19:17:54 +0100449 }
450
Marc Kupietzd36ee552025-05-02 20:42:50 +0200451 resp <- tryCatch(req |> httr2::req_perform(),
452 error = function(e) {
453 if (is.null(e$resp)) {
454 message(paste("Error: ", e$message, collapse = " "), if ("parent" %in% names(e)) paste0("\n", e$parent$message) else "")
455 return(invisible(NULL))
456 }
457 return(e$resp)
458 }
459 )
Marc Kupietz03402e72025-05-02 15:39:40 +0200460
461 if (is.null(resp)) {
Marc Kupietz03402e72025-05-02 15:39:40 +0200462 return(invisible(NULL))
463 }
Marc Kupietz62b17892025-02-01 18:26:45 +0100464
Marc Kupietzf9129592025-01-26 19:17:54 +0100465 if (resp |> httr2::resp_status() != 200) {
Marc Kupietzd36ee552025-05-02 20:42:50 +0200466 message("Error: Request failed with status ", resp |> httr2::resp_status(), ": ", resp |> httr2::resp_status_desc())
Marc Kupietz62b17892025-02-01 18:26:45 +0100467 if (resp |> httr2::resp_content_type() == "application/json") {
468 result <- tryCatch(
469 resp |> httr2::resp_body_json(),
470 error = function(e) {
471 message("Failed to parse json with error details: ", e$message)
472 return(NULL)
473 }
474 )
475 # Handle errors in the response (if any)
476 if (!is.null(result$errors)) {
477 errors <- result$errors
478 warning_msgs <- if (is.data.frame(errors)) {
479 apply(errors, 1, function(warning) paste(warning[1], ": ", warning[2]))
480 } else {
481 lapply(errors, function(error) paste(error, collapse = " "))
482 }
Marc Kupietz03402e72025-05-02 15:39:40 +0200483 message(paste("Warning: ", warning_msgs, collapse = "\n"))
Marc Kupietzf9129592025-01-26 19:17:54 +0100484 }
Marc Kupietzf9129592025-01-26 19:17:54 +0100485 }
Marc Kupietza4675722022-02-23 23:55:15 +0100486 return(invisible(NULL))
487 }
Marc Kupietzf9129592025-01-26 19:17:54 +0100488
489 # Process JSON response or raw text based on `json` parameter
490 if (json) {
491 content_type <- resp |> httr2::resp_content_type()
492 if (!content_type %in% c("application/json", "application/ld+json")) {
493 message("API did not return JSON")
Marc Kupietza4675722022-02-23 23:55:15 +0100494 return(invisible(NULL))
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100495 }
Marc Kupietz04814f22023-04-16 17:13:27 +0200496
Marc Kupietzf9129592025-01-26 19:17:54 +0100497 result <- tryCatch(
498 resp |> httr2::resp_body_string() |> jsonlite::fromJSON(),
499 error = function(e) {
500 message("Failed to parse JSON: ", e$message)
501 return(NULL)
502 }
503 )
504
505 # Handle warnings in the response (if any)
506 if (!is.null(result$warnings)) {
507 warnings <- result$warnings
508 warning_msgs <- if (is.data.frame(warnings)) {
509 apply(warnings, 1, function(warning) paste(warning[1], ": ", warning[2]))
510 } else {
511 lapply(warnings, function(warning) paste(warning, collapse = " "))
512 }
Marc Kupietz03402e72025-05-02 15:39:40 +0200513 message(paste0("\nWarning: ", paste(warning_msgs, collapse = " ")))
514 if (cache & any(grepl("682", warning_msgs))) {
515 cache <- FALSE
Marc Kupietzd36ee552025-05-02 20:42:50 +0200516 log_info(kco@verbose, "Caching will be skipped because of warnings ")
Marc Kupietz03402e72025-05-02 15:39:40 +0200517 }
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100518 }
Marc Kupietzf9129592025-01-26 19:17:54 +0100519 } else {
520 result <- resp |> httr2::resp_body_string()
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200521 }
Marc Kupietzf9129592025-01-26 19:17:54 +0100522
523 # Save to cache if enabled
Marc Kupietz03402e72025-05-02 15:39:40 +0200524 if (cache && resp |> httr2::resp_status() == 200) {
Marc Kupietzb49afa02020-06-04 15:50:29 +0200525 R.cache::saveCache(result, key = list(url, kco@accessToken, kco@indexRevision), dir = KorAPCacheSubDir(), compress = TRUE)
Marc Kupietzb2b32a32020-03-24 13:56:50 +0100526 }
Marc Kupietzf9129592025-01-26 19:17:54 +0100527
528 # Return headers and content as a list if `getHeaders` is TRUE
Marc Kupietzb49afa02020-06-04 15:50:29 +0200529 if (getHeaders) {
Marc Kupietzf9129592025-01-26 19:17:54 +0100530 list(headers = resp |> httr2::resp_headers(), content = result)
Marc Kupietzb49afa02020-06-04 15:50:29 +0200531 } else {
532 result
533 }
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200534})
535
Marc Kupietza824d502025-05-02 15:40:23 +0200536setGeneric("clearCache", function(kco) standardGeneric("clearCache"))
Marc Kupietz0a96b282019-10-01 11:05:31 +0200537
Marc Kupietzdc880ac2025-06-24 20:34:43 +0200538#' Clear local cache
539#'
540#' Clears the local cache of API responses for the current RKorAPClient version.
541#' Useful when you want to force fresh data retrieval or free up disk space.
542#'
543#' @family connection-initialization
544#' @param kco KorAPConnection object
545#' @return Invisible NULL (function called for side effects)
546#' @examples
547#' \dontrun{
548#' kco <- KorAPConnection()
549#' clearCache(kco)
550#' }
Marc Kupietzf9914bb2025-06-25 09:57:55 +0200551#'
Marc Kupietz0a96b282019-10-01 11:05:31 +0200552#' @aliases clearCache
Marc Kupietz0a96b282019-10-01 11:05:31 +0200553#' @export
Marc Kupietza824d502025-05-02 15:40:23 +0200554setMethod("clearCache", "KorAPConnection", function(kco) {
555 R.cache::clearCache(dir = KorAPCacheSubDir())
Marc Kupietz0a96b282019-10-01 11:05:31 +0200556})
557
Marc Kupietza8c40f42025-06-24 15:49:52 +0200558#' Display KorAPConnection object
559#' @keywords internal
Marc Kupietze95108e2019-09-18 13:23:58 +0200560#' @param object KorAPConnection object
561#' @export
562setMethod("show", "KorAPConnection", function(object) {
563 cat("<KorAPConnection>", "\n")
564 cat("apiUrl: ", object@apiUrl, "\n")
565})
566
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200567##' Funtion KorAPConnection()
568##'
Marc Kupietz617266d2025-02-27 10:43:07 +0100569##' Wrappper function for KorAPConnection()
Marc Kupietzd0d3e9b2019-09-24 17:36:03 +0200570##'
571##' @rdname KorAPConnection-constructor
572##' @name KorAPConnection-constructor
573##' @export
Marc Kupietz617266d2025-02-27 10:43:07 +0100574## XKorAPConnection <- function(...) KorAPConnection(...)