From 7a9a9e820c0895e4fc4f4a090ee87627fd76ecd2 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 5 Jan 2019 18:34:55 +0100 Subject: [PATCH 01/55] Add SLUB API, implement parseSearchFields and getSupportFlags This API is specific for SLUB in Dresden. --- .../opacclient/OpacApiFactory.java | 3 + .../de/geeksfactory/opacclient/apis/SLUB.kt | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java index 02deac1ef..d45d6c11d 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java @@ -35,6 +35,7 @@ import de.geeksfactory.opacclient.apis.Primo; import de.geeksfactory.opacclient.apis.SISIS; import de.geeksfactory.opacclient.apis.SLIM; +import de.geeksfactory.opacclient.apis.SLUB; import de.geeksfactory.opacclient.apis.SRU; import de.geeksfactory.opacclient.apis.TestApi; import de.geeksfactory.opacclient.apis.TouchPoint; @@ -141,6 +142,8 @@ public static OpacApi create(Library lib, StringProvider sp, HttpClientFactory h newApiInstance = new Koha(); } else if (lib.getApi().equals("netbiblio")) { newApiInstance = new NetBiblio(); + } else if (lib.getApi().equals("slub")) { + newApiInstance = new SLUB(); } else if (lib.getApi().equals("arena")) { newApiInstance = new Arena(); } else if (lib.getApi().equals("slim")) { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt new file mode 100644 index 000000000..5388a66a8 --- /dev/null +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -0,0 +1,98 @@ +package de.geeksfactory.opacclient.apis + +import de.geeksfactory.opacclient.networking.HttpClientFactory +import de.geeksfactory.opacclient.objects.* +import de.geeksfactory.opacclient.searchfields.SearchField +import de.geeksfactory.opacclient.searchfields.SearchQuery +import de.geeksfactory.opacclient.searchfields.TextSearchField +import de.geeksfactory.opacclient.utils.get +import de.geeksfactory.opacclient.utils.html +import de.geeksfactory.opacclient.utils.text + +/** + * OpacApi implementation for SLUB. https://slub-dresden.de + * + * @author Steffen Rehberg, Jan 2019 + */ +open class SLUB : OkHttpBaseApi() { + protected lateinit var baseurl: String + protected val ENCODING = "UTF-8" + + override fun init(library: Library, factory: HttpClientFactory) { + super.init(library, factory) + baseurl = library.data.getString("baseurl") + } + + override fun search(query: List): SearchRequestResult { + TODO("not implemented") + } + + override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { + TODO("not implemented") + } + + override fun searchGetPage(page: Int): SearchRequestResult { + TODO("not implemented") + } + + override fun getResultById(id: String, homebranch: String?): DetailedItem { + TODO("not implemented") + } + + override fun getResult(position: Int): DetailedItem? { + // getResultById is implemented and every search result has an id set, so getResult is not used + return null + } + + override fun reservation(item: DetailedItem, account: Account, useraction: Int, selection: String?): OpacApi.ReservationResult { + TODO("not implemented") + } + + override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { + TODO("not implemented") + } + + override fun prolongAll(account: Account, useraction: Int, selection: String?): OpacApi.ProlongAllResult { + TODO("not implemented") + } + + override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { + TODO("not implemented") + } + + override fun account(account: Account): AccountData { + TODO("not implemented") + } + + override fun checkAccountData(account: Account) { + TODO("not implemented") + } + + override fun getShareUrl(id: String?, title: String?): String { + TODO("not implemented") + } + + override fun getSupportFlags(): Int { + return 0 + } + + override fun getSupportedLanguages(): Set? { + //TODO("not implemented") + return null + } + + override fun parseSearchFields(): List { + val doc = httpGet(baseurl, ENCODING).html + return doc.select("ul#search-in-field-options li").map { + TextSearchField().apply { + id = it["name"] + displayName = it.text + } + } + } + + override fun setLanguage(language: String?) { + //TODO("not implemented") + } + +} \ No newline at end of file From 38661674ffe815bff88635bd4a45e23939061074 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 2 Feb 2019 23:50:20 +0100 Subject: [PATCH 02/55] Implement search, searchGetPage, getResultById, prolong, account, checkAccountData --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 227 +++++++++++++++++- 1 file changed, 216 insertions(+), 11 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 5388a66a8..dca87667b 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -1,5 +1,6 @@ package de.geeksfactory.opacclient.apis +import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* import de.geeksfactory.opacclient.searchfields.SearchField @@ -8,6 +9,15 @@ import de.geeksfactory.opacclient.searchfields.TextSearchField import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text +import okhttp3.FormBody +import org.joda.time.LocalDateTime +import org.joda.time.format.DateTimeFormat +import org.joda.time.format.DateTimeFormatter +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.jsoup.Jsoup +import org.jsoup.parser.Parser /** * OpacApi implementation for SLUB. https://slub-dresden.de @@ -17,6 +27,35 @@ import de.geeksfactory.opacclient.utils.text open class SLUB : OkHttpBaseApi() { protected lateinit var baseurl: String protected val ENCODING = "UTF-8" + protected lateinit var query: List + + private val mediaTypes = mapOf( + "Article, E-Article" to SearchResult.MediaType.EDOC, + "Book, E-Book" to SearchResult.MediaType.BOOK, + "Video" to SearchResult.MediaType.EVIDEO, + "Thesis" to SearchResult.MediaType.BOOK, + "Manuscript" to SearchResult.MediaType.BOOK, + "Musical Score" to SearchResult.MediaType.SCORE_MUSIC, + "Website" to SearchResult.MediaType.URL, + "Journal, E-Journal" to SearchResult.MediaType.NEWSPAPER, + "Map" to SearchResult.MediaType.MAP, + "Audio" to SearchResult.MediaType.EAUDIO, + "Image" to SearchResult.MediaType.ART, + //"Microfrom" to SearchResult.MediaType.MICROFORM //TODO: define new media type microform + "Visual Media" to SearchResult.MediaType.ART + ) + + private val fieldCaptions = mapOf( + "format" to "Medientyp", + "title" to "Titel", + "contributor" to "Beteiligte", + "publisher" to "Erschienen", + "ispartof" to "Erschienen in", + "identifier" to "ISBN", + "language" to "Sprache", + "subject" to "Schlagwörter", + "description" to "Beschreibung" + ) override fun init(library: Library, factory: HttpClientFactory) { super.init(library, factory) @@ -24,23 +63,121 @@ open class SLUB : OkHttpBaseApi() { } override fun search(query: List): SearchRequestResult { - TODO("not implemented") + this.query = query + return searchGetPage(1) } - override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { - TODO("not implemented") + override fun searchGetPage(page: Int): SearchRequestResult { + val queryfbb = FormBody.Builder() + .add("type", "1369315142") + .add("tx_find_find[format]", "data") + .add("tx_find_find[data-format]", "app") + .add("tx_find_find[page]", page.toString()) + for (sq in query) { + if (sq.value.isNotEmpty()) { + queryfbb.add("tx_find_find[q][${sq.key}]", sq.value) + } + } + val queryfb = queryfbb.build() + if (queryfb.size() <= 4) + throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) + val json = JSONObject(httpPost(baseurl, queryfb, ENCODING)) + return SearchRequestResult(json.optJSONArray("docs") + ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } + ?.map { + SearchResult().apply { + innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) + ?: ""}
(${it.optString("creationDate")})" + type = mediaTypes[it.optJSONArray("format")?.optString(0)] + id = it.optString("id") + } + }, json.optInt("numFound"), 1) + //TODO: get status (one request per item!) } - override fun searchGetPage(page: Int): SearchRequestResult { + override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { TODO("not implemented") } override fun getResultById(id: String, homebranch: String?): DetailedItem { - TODO("not implemented") + + fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = + copiesArray.run { 0.until(length()).map { optJSONObject(it) } } + .map { + Copy().apply { + barcode = it.getString("barcode") + branch = it.getString("location") + department = it.getString("sublocation") // or location = ... + shelfmark = it.getString("shelfmark") + status = Jsoup.parse(it.getString("statusphrase")).text() + it.getString("duedate").run { + if (isNotEmpty()) { + returnDate = df.parseLocalDate(this) + } + } + if (it.getString("vormerken") == "1") { + resInfo = barcode + } + // reservations: only available for reserved copies, not for reservable copies + // url: not for accessible online resources, only for lendable online copies + } + } + + val detailfb = FormBody.Builder() + .add("type", "1369315142") + .add("tx_find_find[format]", "data") + .add("tx_find_find[data-format]", "app") + val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) + val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") + + return DetailedItem().apply { + this.id = id + val record = json.optJSONObject("record") + for (key in record.keys()) { + val v = record.get(key as String) + var value = when (v) { + is String -> v + is Int -> v.toString() + is JSONArray -> 0.until(v.length()).map { + val arrayItem = v.get(it) + when (arrayItem) { + is String -> arrayItem + is JSONObject -> arrayItem.optString("title") + else -> null + } + }.joinToString("; ") + else -> "" + } + if (value.isNotEmpty()) { + value = Parser.unescapeEntities(value, false) + if (key.equals("title")) { + title = value + } + addDetail(Detail(fieldCaptions[key], value)) + } + } + val cps = json.opt("copies") + if (cps is JSONArray) { + getCopies(cps, dateFormat)?.let { copies = it } + } else { // multiple arrays + val copiesList = mutableListOf() + for (key in (cps as JSONObject).keys()) { + val cpsi = cps.get(key as String) + if (cpsi is JSONArray) { + copiesList.addAll(getCopies(cpsi, dateFormat)) + } + } + copies = copiesList + } + // TODO: volumes + // TODO: collectionid + // TODO: add linksAccess as detail (uri & hostLabel, note?, material?) + // TODO: add other links (links, linksRelated, linksGeneral) as details? + } } override fun getResult(position: Int): DetailedItem? { - // getResultById is implemented and every search result has an id set, so getResult is not used + // not used (getResultById is implemented and every search result has an id set) return null } @@ -49,11 +186,16 @@ open class SLUB : OkHttpBaseApi() { } override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { - TODO("not implemented") + return try { + requestAccount(account, "renew", mapOf("tx_slubaccount_account[renewals][0]" to media)) + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK) + } catch (e: Exception) { + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } } override fun prolongAll(account: Account, useraction: Int, selection: String?): OpacApi.ProlongAllResult { - TODO("not implemented") + return OpacApi.ProlongAllResult(OpacApi.MultiStepResult.Status.UNSUPPORTED) } override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { @@ -61,11 +203,74 @@ open class SLUB : OkHttpBaseApi() { } override fun account(account: Account): AccountData { - TODO("not implemented") + val fmt = DateTimeFormat.shortDate() + val json = requestAccount(account, "account") + return AccountData(account.id).apply { + pendingFees = json.optJSONObject("fees")?.optString("topay_list") + validUntil = json.optJSONObject("memberInfo")?.optString("expires") + ?.substring(0, 10)?.let { fmt.print(LocalDateTime(it)) } + lent = json.optJSONObject("items")?.optJSONArray("loan") + ?.run { 0.until(length()).map { optJSONObject(it) } } + ?.map { + LentItem().apply { + title = it.optString("about") + author = it.optJSONArray("X_author")?.optString(0) + setDeadline(it.optString("X_date_due")) + format = it.optString("X_medientyp") + barcode = it.optString("X_barcode") + status = when { + it.optInt("renewals") == 2 -> "2x verlängert" + it.optInt("X_is_reserved") != 0 -> "vorgemerkt" + else -> null + } + isRenewable = if (it.optInt("X_is_renewable") == 1) { + prolongData = barcode + true + } else { + false + } + } + } ?: emptyList() + reservations = json.optJSONObject("items")?.optJSONArray("reserve") + ?.run { 0.until(length()).toMutableList().map { optJSONObject(it) } } + ?.map { + ReservedItem().apply { + title = it.optString("about") + author = it.optJSONArray("X_author")?.optString(0) + format = it.optString("X_medientyp") + status = it.optInt("X_queue_number").let { "Pos. $it" } + } + } ?: emptyList() + } + } + + private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { + val formBody = FormBody.Builder() + .add("type", "1") + .add("tx_slubaccount_account[controller]", "API") + .add("tx_slubaccount_account[action]", action) + .add("tx_slubaccount_account[username]", account.name) + .add("tx_slubaccount_account[password]", account.password) + parameters?.map { + formBody.add(it.key, it.value) + } + try { + return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { + if (it.optInt("status") != 1) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, + it.optString("message", "error requesting account data"))) + } + } + } catch (e: JSONException) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, + "accountRequest didn't return JSON object: ${e.message}")) + } } override fun checkAccountData(account: Account) { - TODO("not implemented") + requestAccount(account, "validate") } override fun getShareUrl(id: String?, title: String?): String { @@ -95,4 +300,4 @@ open class SLUB : OkHttpBaseApi() { //TODO("not implemented") } -} \ No newline at end of file +} From f8ed93d335de358060dcd918b651170aabbff892 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 3 Feb 2019 13:03:51 +0100 Subject: [PATCH 03/55] Implement getShareUrl --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index dca87667b..28b2c414a 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -274,7 +274,7 @@ open class SLUB : OkHttpBaseApi() { } override fun getShareUrl(id: String?, title: String?): String { - TODO("not implemented") + return "$baseurl/id/$id" } override fun getSupportFlags(): Int { From eade6e5bca7bf71dd5b9467464f34e48c222b032 Mon Sep 17 00:00:00 2001 From: StefRe Date: Tue, 5 Feb 2019 20:51:28 +0100 Subject: [PATCH 04/55] Add two more mediatypes --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 28b2c414a..cb3ab5519 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -40,8 +40,9 @@ open class SLUB : OkHttpBaseApi() { "Journal, E-Journal" to SearchResult.MediaType.NEWSPAPER, "Map" to SearchResult.MediaType.MAP, "Audio" to SearchResult.MediaType.EAUDIO, + "Electronic Resource (Data Carrier)" to SearchResult.MediaType.EAUDIO, "Image" to SearchResult.MediaType.ART, - //"Microfrom" to SearchResult.MediaType.MICROFORM //TODO: define new media type microform + "Microform" to SearchResult.MediaType.MICROFORM, "Visual Media" to SearchResult.MediaType.ART ) From 650c90dcc874224554e1c187ae6f490beeffcc14 Mon Sep 17 00:00:00 2001 From: StefRe Date: Mon, 30 Dec 2019 08:02:05 +0100 Subject: [PATCH 05/55] Refactor for testing, add tests for account, search result and detailed item --- opacclient/libopac/build.gradle | 1 + .../de/geeksfactory/opacclient/apis/SLUB.kt | 67 ++- .../geeksfactory/opacclient/apis/SLUBTest.kt | 132 ++++++ .../test/resources/slub/account/account.json | 190 +++++++++ .../resources/slub/account/empty-account.json | 58 +++ .../resources/slub/search/empty-search.json | 47 ++ .../item-copies_in_multiple_arrays.json | 400 ++++++++++++++++++ .../resources/slub/search/simple-item.json | 78 ++++ .../resources/slub/search/simple-search.json | 131 ++++++ 9 files changed, 1081 insertions(+), 23 deletions(-) create mode 100644 opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt create mode 100644 opacclient/libopac/src/test/resources/slub/account/account.json create mode 100644 opacclient/libopac/src/test/resources/slub/account/empty-account.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/empty-search.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/simple-item.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/simple-search.json diff --git a/opacclient/libopac/build.gradle b/opacclient/libopac/build.gradle index ea2bd94b2..23c54f94f 100644 --- a/opacclient/libopac/build.gradle +++ b/opacclient/libopac/build.gradle @@ -23,6 +23,7 @@ dependencies { testImplementation 'org.hamcrest:hamcrest-library:1.3' testImplementation 'org.mockito:mockito-core:1.10.19' compile "org.jetbrains.kotlin:kotlin-stdlib:1.3.11" + testImplementation 'com.shazam:shazamcrest:0.11' } task copyTestResources(type: Copy) { from "${projectDir}/src/test/resources", "${projectDir}/src/main/resources" diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index cb3ab5519..ecd791d31 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -83,8 +83,11 @@ open class SLUB : OkHttpBaseApi() { if (queryfb.size() <= 4) throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) val json = JSONObject(httpPost(baseurl, queryfb, ENCODING)) - return SearchRequestResult(json.optJSONArray("docs") - ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } + return parseSearchResults(json) + } + + internal fun parseSearchResults(json: JSONObject): SearchRequestResult{ + val searchresults = json.optJSONArray("docs")?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } ?.map { SearchResult().apply { innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) @@ -92,8 +95,9 @@ open class SLUB : OkHttpBaseApi() { type = mediaTypes[it.optJSONArray("format")?.optString(0)] id = it.optString("id") } - }, json.optInt("numFound"), 1) + } //TODO: get status (one request per item!) + return SearchRequestResult(searchresults, json.optInt("numFound"), 1) } override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { @@ -101,7 +105,16 @@ open class SLUB : OkHttpBaseApi() { } override fun getResultById(id: String, homebranch: String?): DetailedItem { + val detailfb = FormBody.Builder() + .add("type", "1369315142") + .add("tx_find_find[format]", "data") + .add("tx_find_find[data-format]", "app") + val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) + return parseResultById(id, json) + } + internal fun parseResultById(id:String, json: JSONObject): DetailedItem { + val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = copiesArray.run { 0.until(length()).map { optJSONObject(it) } } .map { @@ -123,14 +136,6 @@ open class SLUB : OkHttpBaseApi() { // url: not for accessible online resources, only for lendable online copies } } - - val detailfb = FormBody.Builder() - .add("type", "1369315142") - .add("tx_find_find[format]", "data") - .add("tx_find_find[data-format]", "app") - val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) - val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") - return DetailedItem().apply { this.id = id val record = json.optJSONObject("record") @@ -204,13 +209,38 @@ open class SLUB : OkHttpBaseApi() { } override fun account(account: Account): AccountData { - val fmt = DateTimeFormat.shortDate() val json = requestAccount(account, "account") + return parseAccountData(account, json) + } + + internal fun parseAccountData(account: Account, json: JSONObject): AccountData { + fun getReservations(items: JSONObject?): MutableList { + val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve", "ill") + // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" + val reservationsList = mutableListOf() + for (type in types) { + items?.optJSONArray(type)?.let { + for (i in 0 until it.length()) { + reservationsList.add(it.getJSONObject(i).let { + ReservedItem().apply { + title = it.optString("about") + author = it.optJSONArray("X_author")?.optString(0) + format = it.optString("X_medientyp") + status = it.optInt("X_queue_number").let { "Pos. $it" } + } + }) + } + } + } + return reservationsList + } + + val fmt = DateTimeFormat.shortDate() return AccountData(account.id).apply { pendingFees = json.optJSONObject("fees")?.optString("topay_list") validUntil = json.optJSONObject("memberInfo")?.optString("expires") ?.substring(0, 10)?.let { fmt.print(LocalDateTime(it)) } - lent = json.optJSONObject("items")?.optJSONArray("loan") + lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } ?.map { LentItem().apply { @@ -232,16 +262,7 @@ open class SLUB : OkHttpBaseApi() { } } } ?: emptyList() - reservations = json.optJSONObject("items")?.optJSONArray("reserve") - ?.run { 0.until(length()).toMutableList().map { optJSONObject(it) } } - ?.map { - ReservedItem().apply { - title = it.optString("about") - author = it.optJSONArray("X_author")?.optString(0) - format = it.optString("X_medientyp") - status = it.optInt("X_queue_number").let { "Pos. $it" } - } - } ?: emptyList() + reservations = getReservations(json.optJSONObject("items")) } } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt new file mode 100644 index 000000000..a3b354686 --- /dev/null +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -0,0 +1,132 @@ +package de.geeksfactory.opacclient.apis + +import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs +import de.geeksfactory.opacclient.objects.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + + +class SLUBAccountTest() : BaseHtmlTest() { + var slub = SLUB() + + @Test + fun testParseEmptyAccountData() { + val json = JSONObject(readResource("/slub/account/empty-account.json")) + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals("1,23 EUR", accountdata.pendingFees) + assertEquals("31.03.20", accountdata.validUntil) + assertTrue(accountdata.lent.isEmpty()) + assertTrue(accountdata.reservations.isEmpty()) + } + + @Test + fun testParseAccountData() { + val json = JSONObject(readResource("/slub/account/account.json")) + val lentitem1 = LentItem().apply { + title = "¬Der¬ neue Kosmos-Baumführer" + author = "Bachofer, Mark" + setDeadline("2019-06-03") + format = "B" + barcode = "31626878" + isRenewable = true + prolongData = barcode + } + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals(2, accountdata.lent.size) + assertEquals(3, accountdata.reservations.size) + assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) + assertEquals("vorgemerkt", accountdata.lent[1].status) + } +} + +class SLUBSearchTest() : BaseHtmlTest() { + var slub = SLUB() + + @Test + fun testParseEmptySearchResults() { + val json = JSONObject(readResource("/slub/search/empty-search.json")) + + val searchresults = slub.parseSearchResults(json) + + assertEquals(0, searchresults.total_result_count) + assertTrue(searchresults.results.isEmpty()) + } + + @Test + fun testParseSearchResults() { + val json = JSONObject(readResource("/slub/search/simple-search.json")) + val result1 = SearchResult().apply { + innerhtml = "Mastering software testing with JUnit 5 comprehensive guide to develop high quality Java applications Boni García
Garcia, Boni
(2017)" + type = SearchResult.MediaType.BOOK + id = "0-1014939550" + } + + val searchresults = slub.parseSearchResults(json) + + assertEquals(2, searchresults.total_result_count) + assertThat(result1, samePropertyValuesAs(searchresults.results[0])) + } + + @Test + fun testParseResultById() { + val json = JSONObject(readResource("/slub/search/simple-item.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Buch")) + addDetail(Detail("Titel", "Unit-Tests mit JUnit")) + title = "Unit-Tests mit JUnit" + addDetail(Detail("Beteiligte", "Hunt, Andrew; Thomas, David [Autor/In]")) + addDetail(Detail("Erschienen", "München Wien Hanser 2004 ")) + addDetail(Detail("Erschienen in", "Pragmatisch Programmieren; 2")) + addDetail(Detail("ISBN", "3446228241; 3446404694; 9783446404694; 9783446228245")) + addDetail(Detail("Sprache", "Deutsch")) + addDetail(Detail("Schlagwörter", "Quellcode; Softwaretest; JUnit")) + id = "0-1182402208" + copies = arrayListOf(Copy().apply { + barcode = "31541466" + department = "Freihand" + branch = "Bereichsbibliothek DrePunct" + status = "Ausleihbar" + shelfmark = "ST 233 H939" + }) + } + + val item = slub.parseResultById(json.getString("id"), json) + + //details are in unspecified order, see https://stackoverflow.com/a/4920304/3944322 + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(item.details), sameBeanAs(HashSet(expected.details))) + } + + @Test + fun testParseResultByIdCopiesInMultipleArrays() { + val json = JSONObject(readResource("/slub/search/item-copies_in_multiple_arrays.json")) + val copyFirst = Copy().apply { + barcode = "10418078" + department = "Magazin Zeitschriften" + branch = "Zentralbibliothek" + status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" + shelfmark = "19 4 01339 0 0024 1 01" + } + val copyLast = Copy().apply { + barcode = "33364639" + department = "Magazin Zeitschriften" + branch = "Zentralbibliothek" + status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" + shelfmark = "19 4 01339 1 1969 1 01" + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertEquals(19, item.copies.size) + // the copies arrays may occur in any order + assertThat(item.copies, hasItems(sameBeanAs(copyFirst), sameBeanAs(copyLast))) + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/account/account.json b/opacclient/libopac/src/test/resources/slub/account/account.json new file mode 100644 index 000000000..f1286a15c --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/account.json @@ -0,0 +1,190 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2099-01-01T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65143", + "X_date_of_last_issue_ext": "2019-05-10", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "items": { + "loan": [ + { + "status": 3, + "about": "¬Der¬ neue Kosmos-Baumführer", + "label": "31626878", + "queue": "0", + "renewals": 0, + "starttime": "2019-05-05T15:45:39Z", + "endtime": "2019-06-02T22:00:00Z", + "X_status_desc": "on loan", + "X_barcode": "31626878", + "X_author": [ + "Bachofer, Mark", + "Mayer, Joachim" + ], + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_shelfLocation": "WL 9825 B124", + "X_internal_date_issued": "65138,63939", + "X_date_issued": "2019-05-05 17:45:39", + "X_internal_date_due": "65167", + "X_date_due": "2019-06-03", + "X_days_to_due": "20", + "X_is_reserved": 0, + "X_is_renewable": 1, + "X_is_flrenewable": 0, + "X_rsn": 13840872 + }, + { + "status": 3, + "about": "Bäume bestimmen", + "label": "33121334", + "queue": "0", + "renewals": 0, + "starttime": "2019-04-30T10:37:11Z", + "endtime": "2019-05-27T22:00:00Z", + "X_status_desc": "on loan", + "X_barcode": "33121334", + "X_author": [ + "Lüder, Rita" + ], + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_shelfLocation": "WL 9825 L948", + "X_internal_date_issued": "65133,45431", + "X_date_issued": "2019-04-30 12:37:11", + "X_internal_date_due": "65161", + "X_date_due": "2019-05-28", + "X_days_to_due": "14", + "X_is_reserved": 1, + "X_is_renewable": 1, + "X_is_flrenewable": 0, + "X_rsn": 15864711 + } + ], + "reserve": [ + { + "status": 1, + "about": "Pareys Buch der Bäume", + "label": "30963742", + "starttime": "2019-05-10T07:11:09Z", + "X_queue_number": 1, + "X_delete_number": 1, + "X_author": [ + "Mitchell, Alan", + "Wilkinson, John", + "Schütt, Peter ¬[Übers.]¬" + ], + "X_status_desc": "reserved", + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_internal_date_reserved": "65143,33069", + "X_date_reserved": "2019-05-10 09:11:09", + "X_provided": 0, + "X_rsn": 963145 + } + ], + "hold": [ + { + "status": 4, + "about": "Welcher Baum ist das?", + "label": "34778398", + "starttime": "2019-05-09T22:00:00Z", + "X_status_desc": "on hold", + "X_pickup_code": "tha1", + "X_pickup_desc": "ZwB Forstwissenschaft", + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_ill_duedate": "", + "X_ill_request_id": "", + "X_author": [ + "Mayer, Joachim ¬[VerfasserIn]¬", + "Schwegler, Heinz Werner ¬[VerfasserIn]¬" + ], + "X_internal_date_reserved": "65143", + "X_date_reserved": "2019-05-10", + "X_provided": 0, + "X_rsn": 16605483 + } + ], + "request_ready": [ + { + "status": 1, + "about": "Englische Synonyme als Fehlerquellen", + "label": "20550495", + "starttime": "2019-05-04T06:35:39Z", + "X_status_desc": "requested", + "X_author": [ + "Meyer, Jürgen", + "Schulz, Gisela" + ], + "X_stackid": "2038188", + "X_systemmessage": "http://libero6.slub-dresden.de:57772/csp/user/paia/coremsg.csp?patronid=4222666&msgid=2038188", + "X_medientyp": "B", + "X_exstatus": "NM", + "X_exstatus_desc": "Ausleihbar ", + "X_internal_date_requested": "65137,30939", + "X_date_requested": "2019-05-04 08:35:39", + "X_pickup_code": "a01", + "X_pickup_desc": "Zentralbibliothek Ebene 0 SB-Regal", + "X_stacklocation": "m002", + "X_request": "l", + "X_request_desc": "liegt bereit", + "X_rsn": 1494121 + } + ] + }, + "fees": { + "amount": "0,00 EUR", + "subset": "open", + "amount_list": "0,00 EUR", + "paid_all": "0,00 EUR", + "paid_list": "0,00 EUR", + "fee_all": "0,00 EUR", + "fee_list": "0,00 EUR", + "topay_all": "0,00 EUR", + "topay_list": "0,00 EUR", + "overdue_paid": "0,00 EUR", + "overdue_fee": "0,00 EUR", + "ill_paid": "0,00 EUR", + "ill_fee": "0,00 EUR", + "other_paid": "0,00 EUR", + "other_fee": "0,00 EUR", + "fee": [] + }, + "arguments": { + "controller": "API", + "action": "account", + "username": "1234567" + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/account/empty-account.json b/opacclient/libopac/src/test/resources/slub/account/empty-account.json new file mode 100644 index 000000000..4f52b21f6 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/empty-account.json @@ -0,0 +1,58 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2020-03-31T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65143", + "X_date_of_last_issue_ext": "2019-05-10", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "fees": { + "amount": "0,00 EUR", + "subset": "open", + "amount_list": "0,00 EUR", + "paid_all": "0,00 EUR", + "paid_list": "0,00 EUR", + "fee_all": "0,00 EUR", + "fee_list": "0,00 EUR", + "topay_all": "0,00 EUR", + "topay_list": "1,23 EUR", + "overdue_paid": "0,00 EUR", + "overdue_fee": "0,00 EUR", + "ill_paid": "0,00 EUR", + "ill_fee": "0,00 EUR", + "other_paid": "0,00 EUR", + "other_fee": "0,00 EUR", + "fee": [] + }, + "arguments": { + "controller": "API", + "action": "account", + "username": "1234567" + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/empty-search.json b/opacclient/libopac/src/test/resources/slub/search/empty-search.json new file mode 100644 index 000000000..bbee79a08 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/empty-search.json @@ -0,0 +1,47 @@ +{ + "numFound": 0, + "start": 0, + "docs": [], + "facets": { + "access_facet": { + "translation": "Zugang", + "values": [] + }, + "format_de15": { + "translation": "Medientyp", + "values": [] + }, + "publishDateSort": { + "translation": "Erscheinungsjahr", + "values": [] + }, + "branch": { + "translation": "Standort", + "values": [] + }, + "language": { + "translation": "Sprache", + "values": [] + }, + "thema": { + "translation": "Fachgebiet", + "values": [] + }, + "author": { + "translation": "Person/Institution", + "values": [] + }, + "facet_music_notation_de14": { + "translation": "musikalische Ausgabeform", + "values": [] + }, + "music_heading_browse": { + "translation": "musikalische Besetzung", + "values": [] + }, + "mega_collection": { + "translation": "Kollektion", + "values": [] + } + } +} diff --git a/opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json b/opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json new file mode 100644 index 000000000..534d88f29 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json @@ -0,0 +1,400 @@ +{ + "record": { + "format": "Zeitschrift", + "title": "Thyssen Technische Berichte", + "contributor": [ + "Thyssen-Aktiengesellschaft, Vormals August-Thyssen-Hütte", + "Thyssen-Gruppe", + "Thyssen-Stahl-Aktiengesellschaft" + ], + "publisher": [ + "Duisburg Thyssen 1974-1993 " + ], + "ispartof": [], + "identifier": "", + "language": [ + "Deutsch" + ], + "subject": [ + "Stahl", + "Zeitschrift", + "Werkstoffkunde", + "Zeitschrift", + "Eisen- und Stahlindustrie", + "Zeitschrift" + ], + "description": [ + "Beteil. Körp. 6.1974 - 8.1976: August-Thyssen-Hütte, Abteilung Zentrale Forschung der Thyssen-Gruppe; 9.1977 - 21.1989: Thyssen-Aktiengesellschaft, vormals August-Thyssen-Hütte." + ], + "status": "", + "rvk": "" + }, + "id": "0-130446319", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [ + { + "text": "Vorgänger", + "link": "http://slubdd.de/katalog?libero_mab21364124", + "name": "August-Thyssen-Hütte: Thyssen-Forschung", + "target": "SLUB" + } + ], + "copies": { + "1990 - 1999": [ + { + "barcode": "10418078", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0024 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 24.1992", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10076153", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0023 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 23.1991", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10076694", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0022 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 22.1990", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "1980 - 1989": [ + { + "barcode": "10075519", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0021 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 21.1989", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10076756", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0020 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 20.1988", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10077027", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0019 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 19.1987", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10075809", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0018 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 18.1986", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364662", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0017 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 17.1985", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364651", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0016 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 16.1984", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364640", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0015 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 15.1983", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364663", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0014 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 14.1982", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364652", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0013 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 13.1981", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364641", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0012 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 12.1980,1", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "1970 - 1979": [ + { + "barcode": "33364140", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0011 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 11.1979", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364141", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0010 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 10.1978", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364152", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0009 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 9.1977", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364153", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0008 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 8.1976,1", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364142", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0007 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 7.1975,1", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "1960 - 1969": [ + { + "barcode": "33364639", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 1 1969 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 1969/78,Index", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ] + }, + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/simple-item.json b/opacclient/libopac/src/test/resources/slub/search/simple-item.json new file mode 100644 index 000000000..ddee76619 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/simple-item.json @@ -0,0 +1,78 @@ +{ + "record": { + "format": "Buch", + "title": "Unit-Tests mit JUnit", + "contributor": [ + "Hunt, Andrew", + "Thomas, David [Autor/In]" + ], + "publisher": [ + "München Wien Hanser 2004 " + ], + "ispartof": [ + { + "id": "0-1183957874", + "title": "Pragmatisch Programmieren; 2" + } + ], + "identifier": [ + "3446228241", + "3446404694", + "9783446404694", + "9783446228245" + ], + "language": [ + "Deutsch" + ], + "subject": [ + "Quellcode", + "Softwaretest", + "JUnit" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "0-1182402208", + "oa": 0, + "thumbnail": "", + "links": [ + { + "uri": "http://d-nb.info/970689268/04", + "note": "", + "material": "Inhaltsverzeichnis" + } + ], + "linksRelated": [ + { + "uri": "http://d-nb.info/970689268/04", + "hostLabel": "", + "note": "", + "material": "Inhaltsverzeichnis" + } + ], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [ + { + "barcode": "31541466", + "location": "Bereichsbibliothek DrePunct", + "location_code": "zell9", + "sublocation": "Freihand", + "shelfmark": "ST 233 H939", + "mediatype": "B", + "3d": "ST 233 H939", + "3d_link": "https://3d.slub-dresden.de/viewer?project_id=3&search_key=ST%20233%20H939&language=de&search_context1=zell9&search_context2=FH1&exemplar_id=31541466", + "issue": "", + "colorcode": "1", + "statusphrase": "Ausleihbar", + "link": "", + "status": "N", + "duedate": "", + "vormerken": "0", + "bestellen": "0" + } + ], + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/simple-search.json b/opacclient/libopac/src/test/resources/slub/search/simple-search.json new file mode 100644 index 000000000..689fe67f5 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/simple-search.json @@ -0,0 +1,131 @@ +{ + "numFound": 2, + "start": 0, + "docs": [ + { + "id": "0-1014939550", + "format": [ + "Book, E-Book" + ], + "title": "Mastering software testing with JUnit 5 comprehensive guide to develop high quality Java applications Boni García", + "author": [ + "Garcia, Boni" + ], + "creationDate": "2017", + "imprint": [ + "Birmingham, UK: Packt Publishing, 2017" + ] + }, + { + "id": "dswarm-141-NzE4NjM5", + "format": [ + "Video" + ], + "title": "Learning JUnit 5", + "author": [ + "Boni García" + ], + "creationDate": "2018", + "imprint": [ + "Carpenteria : Lynda, 2018" + ] + } + ], + "facets": { + "access_facet": { + "translation": "Zugang", + "values": { + "Electronic Resources": { + "translation": "Online-Ressourcen", + "values": 2 + } + } + }, + "format_de15": { + "translation": "Medientyp", + "values": { + "Book, E-Book": { + "translation": "Bücher", + "values": 1 + }, + "Video": { + "translation": "Video", + "values": 1 + } + } + }, + "publishDateSort": { + "translation": "Erscheinungsjahr", + "values": { + "2017": { + "translation": "2017", + "values": 1 + }, + "2018": { + "translation": "2018", + "values": 1 + } + } + }, + "branch": { + "translation": "Standort", + "values": [] + }, + "language": { + "translation": "Sprache", + "values": { + "English": { + "translation": "Englisch", + "values": 2 + } + } + }, + "thema": { + "translation": "Fachgebiet", + "values": { + "Informatik": { + "translation": "Informatik", + "values": 1 + }, + "Mathematik": { + "translation": "Mathematik", + "values": 1 + } + } + }, + "author": { + "translation": "Person/Institution", + "values": { + "Boni García": { + "translation": "Boni García", + "values": 1 + }, + "Garcia, Boni": { + "translation": "Garcia, Boni", + "values": 1 + } + } + }, + "facet_music_notation_de14": { + "translation": "musikalische Ausgabeform", + "values": [] + }, + "music_heading_browse": { + "translation": "musikalische Besetzung", + "values": [] + }, + "mega_collection": { + "translation": "Kollektion", + "values": { + "Lynda.com Englisch": { + "translation": "Lynda.com Englisch", + "values": 1 + }, + "Verbunddaten SWB": { + "translation": "Verbunddaten SWB", + "values": 1 + } + } + } + } +} From 6fc988e25c0af9847f767d9b4a317d01f1841de5 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Fri, 3 Jan 2020 12:45:49 +0100 Subject: [PATCH 06/55] Include year in search results only if not "null" --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 7 +- .../geeksfactory/opacclient/apis/SLUBTest.kt | 15 +++ .../search/search-null_creation_date.json | 111 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index ecd791d31..f3efcd571 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -91,7 +91,12 @@ open class SLUB : OkHttpBaseApi() { ?.map { SearchResult().apply { innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) - ?: ""}
(${it.optString("creationDate")})" + ?: ""}" + it.optString("creationDate")?.run { + if (this != "null") { + innerhtml += "
(${this})" + } + } type = mediaTypes[it.optJSONArray("format")?.optString(0)] id = it.optString("id") } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index a3b354686..ff8ff0e4c 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -75,6 +75,21 @@ class SLUBSearchTest() : BaseHtmlTest() { assertThat(result1, samePropertyValuesAs(searchresults.results[0])) } + @Test + fun testParseSearchResultsWithNullCreationDate() { + val json = JSONObject(readResource("/slub/search/search-null_creation_date.json")) + val result1 = SearchResult().apply { + innerhtml = "Tu en hagiois patros hēmōn Maximu tu homologetu Hapanta = S.P.N. Maximi Confessoris Opera omnia eruta, Latine transl., notisque ill. opera et studio Francisci Combefis. Adauxit Franciscus Oehler. Accurante et denuo recognoscente J.-P. Migne
Maximus Confessor" + type = SearchResult.MediaType.BOOK + id = "0-1093989777" + } + + val searchresults = slub.parseSearchResults(json) + + assertEquals(1, searchresults.total_result_count) + assertThat(result1, samePropertyValuesAs(searchresults.results[0])) + } + @Test fun testParseResultById() { val json = JSONObject(readResource("/slub/search/simple-item.json")) diff --git a/opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json b/opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json new file mode 100644 index 000000000..0549bb010 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json @@ -0,0 +1,111 @@ +{ + "numFound": 1, + "start": 0, + "docs": [ + { + "id": "0-1093989777", + "format": [ + "Book, E-Book" + ], + "title": "Tu en hagiois patros hēmōn Maximu tu homologetu Hapanta = S.P.N. Maximi Confessoris Opera omnia eruta, Latine transl., notisque ill. opera et studio Francisci Combefis. Adauxit Franciscus Oehler. Accurante et denuo recognoscente J.-P. Migne", + "author": [ + "Maximus Confessor", + "Combefis, François", + "Oehler, Franz", + "Migne, Jacques Paul" + ], + "creationDate": null, + "imprint": [ + "Lutetiae Parisiorum: Migne, 18XX" + ] + } + ], + "facets": { + "access_facet": { + "translation": "Zugang", + "values": { + "Local Holdings": { + "translation": "Lokale Bestände", + "values": 1 + } + } + }, + "format_de15": { + "translation": "Medientyp", + "values": { + "Book, E-Book": { + "translation": "Bücher", + "values": 1 + } + } + }, + "publishDateSort": { + "translation": "Erscheinungsjahr", + "values": [] + }, + "branch": { + "translation": "Standort", + "values": [] + }, + "language": { + "translation": "Sprache", + "values": { + "Ancient Greek": { + "translation": "Altgriechisch", + "values": 1 + }, + "Latin": { + "translation": "Latein", + "values": 1 + } + } + }, + "thema": { + "translation": "Fachgebiet", + "values": { + "Theologie und Religionswissenschaft": { + "translation": "Theologie und Religionswissenschaft", + "values": 1 + } + } + }, + "author": { + "translation": "Person/Institution", + "values": { + "Combefis, François": { + "translation": "Combefis, François", + "values": 1 + }, + "Maximus Confessor": { + "translation": "Maximus Confessor", + "values": 1 + }, + "Migne, Jacques Paul": { + "translation": "Migne, Jacques Paul", + "values": 1 + }, + "Oehler, Franz": { + "translation": "Oehler, Franz", + "values": 1 + } + } + }, + "facet_music_notation_de14": { + "translation": "musikalische Ausgabeform", + "values": [] + }, + "music_heading_browse": { + "translation": "musikalische Besetzung", + "values": [] + }, + "mega_collection": { + "translation": "Kollektion", + "values": { + "Verbunddaten SWB": { + "translation": "Verbunddaten SWB", + "values": 1 + } + } + } + } +} \ No newline at end of file From 8215756289d79b28732a837d4bb8c01781a6e283 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Fri, 3 Jan 2020 16:50:23 +0100 Subject: [PATCH 07/55] Correctly process reserved inter-library loan items --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 16 +++- .../geeksfactory/opacclient/apis/SLUBTest.kt | 18 +++++ .../slub/account/account-ill_in_progress.json | 76 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index f3efcd571..5891a60bc 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -220,7 +220,7 @@ open class SLUB : OkHttpBaseApi() { internal fun parseAccountData(account: Account, json: JSONObject): AccountData { fun getReservations(items: JSONObject?): MutableList { - val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve", "ill") + val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" val reservationsList = mutableListOf() for (type in types) { @@ -237,6 +237,20 @@ open class SLUB : OkHttpBaseApi() { } } } + items?.optJSONArray("ill")?.let { + for (i in 0 until it.length()) { + reservationsList.add(it.getJSONObject(i).let { + ReservedItem().apply { + title = it.optString("Titel") + author = it.optString("Autor") + id = it.optString("Fernleih_ID") + branch = it.optString("Zweigstelle") + status = it.optString("Status_DESC") + } + }) + } + } + return reservationsList } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index ff8ff0e4c..04e0249f3 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -45,6 +45,24 @@ class SLUBAccountTest() : BaseHtmlTest() { assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) assertEquals("vorgemerkt", accountdata.lent[1].status) } + + @Test + fun testParseAccountDataIllInProgress() { + val json = JSONObject(readResource("/slub/account/account-ill_in_progress.json")) + val reserveditem = ReservedItem().apply { + title = "Kotlin" + author = "Szwillus, Karl" + id = "145073" + branch = "zell1" + status = "Bestellung ausgelöst" + } + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals(0, accountdata.lent.size) + assertEquals(1, accountdata.reservations.size) + assertThat(reserveditem, samePropertyValuesAs(accountdata.reservations[0])) + } } class SLUBSearchTest() : BaseHtmlTest() { diff --git a/opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json b/opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json new file mode 100644 index 000000000..6dd2bf74e --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json @@ -0,0 +1,76 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2020-03-31T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65375", + "X_date_of_last_issue_ext": "2019-12-28", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "items": { + "ill": [ + { + "Fernleih_ID": "145073", + "Titel": "Kotlin", + "Autor": "Szwillus, Karl", + "Datum_intern": "65381", + "Datum_extern": "2020-01-02T23:00:00Z", + "Datum_Ausleihe_intern": "65381", + "Datum_Ausleihe_extern": "2020-01-02T23:00:00Z", + "Zweigstelle": "zell1", + "Status": "2", + "Status_DESC": "Bestellung ausgelöst", + "Medientyp": "", + "History": [] + } + ] + }, + "fees": { + "amount": "0,00 EUR", + "subset": "open", + "amount_list": "0,00 EUR", + "paid_all": "0,00 EUR", + "paid_list": "0,00 EUR", + "fee_all": "0,00 EUR", + "fee_list": "0,00 EUR", + "topay_all": "0,00 EUR", + "topay_list": "0,00 EUR", + "overdue_paid": "0,00 EUR", + "overdue_fee": "0,00 EUR", + "ill_paid": "0,00 EUR", + "ill_fee": "0,00 EUR", + "other_paid": "0,00 EUR", + "other_fee": "0,00 EUR", + "fee": [] + }, + "arguments": { + "controller": "API", + "action": "account", + "username": "1234567" + } +} \ No newline at end of file From ce83f9a1800a53e29ef2a018925847b67e4307e9 Mon Sep 17 00:00:00 2001 From: StefRe Date: Fri, 3 Jan 2020 22:13:43 +0100 Subject: [PATCH 08/55] Make test of expiry date work independently of locale Format expected date value according to current locale (required for CI testing) --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 4 ++-- .../src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 5891a60bc..680f552ba 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -10,7 +10,7 @@ import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text import okhttp3.FormBody -import org.joda.time.LocalDateTime +import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter import org.json.JSONArray @@ -258,7 +258,7 @@ open class SLUB : OkHttpBaseApi() { return AccountData(account.id).apply { pendingFees = json.optJSONObject("fees")?.optString("topay_list") validUntil = json.optJSONObject("memberInfo")?.optString("expires") - ?.substring(0, 10)?.let { fmt.print(LocalDateTime(it)) } + ?.substring(0, 10)?.let { fmt.print(LocalDate(it)) } lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } ?.map { diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 04e0249f3..c09e8d844 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -4,6 +4,8 @@ import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs import de.geeksfactory.opacclient.objects.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -20,7 +22,7 @@ class SLUBAccountTest() : BaseHtmlTest() { val accountdata = slub.parseAccountData(Account(), json) assertEquals("1,23 EUR", accountdata.pendingFees) - assertEquals("31.03.20", accountdata.validUntil) + assertEquals(DateTimeFormat.shortDate().print(LocalDate("2020-03-31")), accountdata.validUntil) assertTrue(accountdata.lent.isEmpty()) assertTrue(accountdata.reservations.isEmpty()) } From 74fe9fc4ab8b2b681fc2cb10040421f13033c95e Mon Sep 17 00:00:00 2001 From: StefRe Date: Mon, 13 Jan 2020 21:12:23 +0100 Subject: [PATCH 09/55] implement cancel --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 680f552ba..badf65078 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -210,7 +210,12 @@ open class SLUB : OkHttpBaseApi() { } override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { - TODO("not implemented") + return try { + requestAccount(account, "delete", mapOf("tx_slubaccount_account[delete][0]" to media)) + OpacApi.CancelResult(OpacApi.MultiStepResult.Status.OK) + } catch (e: Exception) { + OpacApi.CancelResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } } override fun account(account: Account): AccountData { From 118089a48a49dfea67ef3e07b8df5c9e7f79608d Mon Sep 17 00:00:00 2001 From: StefRe Date: Mon, 13 Jan 2020 21:19:56 +0100 Subject: [PATCH 10/55] Change search method from POST to GET If the search yields exactly one result then the POST method doesn't return a JSON object but the html catalogue detail web site of this item. Using GET handles this case correctly (returns JSON with one item). --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index badf65078..819ac6ce7 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -10,6 +10,7 @@ import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text import okhttp3.FormBody +import okhttp3.HttpUrl import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -69,21 +70,24 @@ open class SLUB : OkHttpBaseApi() { } override fun searchGetPage(page: Int): SearchRequestResult { - val queryfbb = FormBody.Builder() - .add("type", "1369315142") - .add("tx_find_find[format]", "data") - .add("tx_find_find[data-format]", "app") - .add("tx_find_find[page]", page.toString()) + if (query.size <= 4) { + throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) + } + val queryUrlB = HttpUrl.get("$baseurl/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app") + .newBuilder() + .addQueryParameter("tx_find_find[page]", page.toString()) for (sq in query) { if (sq.value.isNotEmpty()) { - queryfbb.add("tx_find_find[q][${sq.key}]", sq.value) + queryUrlB.addQueryParameter("tx_find_find[q][${sq.key}]", sq.value) } } - val queryfb = queryfbb.build() - if (queryfb.size() <= 4) - throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) - val json = JSONObject(httpPost(baseurl, queryfb, ENCODING)) - return parseSearchResults(json) + try { + return parseSearchResults(JSONObject(httpGet(queryUrlB.build().toString(), ENCODING))) + } catch (e: JSONException) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_WITH_DESCRIPTION, + "search returned malformed JSON object: ${e.message}")) + } } internal fun parseSearchResults(json: JSONObject): SearchRequestResult{ From 8c8500d7e6cd73bf2275b32875cf3dee82712f68 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 15 Jan 2020 21:40:26 +0100 Subject: [PATCH 11/55] Add MIT license header as requested at https://github.com/opacapp/opacclient/wiki --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 819ac6ce7..9268c831f 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -1,3 +1,24 @@ +/** + * Copyright (C) 2020 by Steffen Rehberg under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ package de.geeksfactory.opacclient.apis import de.geeksfactory.opacclient.i18n.StringProvider From d18931b32d337cfeff279427e175c0c13bd0aea5 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 15 Jan 2020 22:22:04 +0100 Subject: [PATCH 12/55] Change all http requests methods from POST to GET As of Jan 15, 2020, all account POST requests get redirected to the web catalogue login page and POST requests for all search requests get redirected to the html result pages of the web catalogue. --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 9268c831f..68445fba3 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -30,7 +30,6 @@ import de.geeksfactory.opacclient.searchfields.TextSearchField import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text -import okhttp3.FormBody import okhttp3.HttpUrl import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat @@ -135,11 +134,16 @@ open class SLUB : OkHttpBaseApi() { } override fun getResultById(id: String, homebranch: String?): DetailedItem { - val detailfb = FormBody.Builder() - .add("type", "1369315142") - .add("tx_find_find[format]", "data") - .add("tx_find_find[data-format]", "app") - val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) + val json: JSONObject + try { + json = JSONObject(httpGet( + "$baseurl/id/$id/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app", + ENCODING)) + } catch (e: JSONException) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_WITH_DESCRIPTION, + "search returned malformed JSON object: ${e.message}")) + } return parseResultById(id, json) } @@ -316,17 +320,16 @@ open class SLUB : OkHttpBaseApi() { } private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { - val formBody = FormBody.Builder() - .add("type", "1") - .add("tx_slubaccount_account[controller]", "API") - .add("tx_slubaccount_account[action]", action) - .add("tx_slubaccount_account[username]", account.name) - .add("tx_slubaccount_account[password]", account.password) + val queryUrlB = HttpUrl.get("$baseurl/mein-konto/?type=1&tx_slubaccount_account[controller]=API") + .newBuilder() + .addQueryParameter("tx_slubaccount_account[action]", action) + .addQueryParameter("tx_slubaccount_account[username]", account.name) + .addQueryParameter("tx_slubaccount_account[password]", account.password) parameters?.map { - formBody.add(it.key, it.value) + queryUrlB.addQueryParameter(it.key, it.value) } try { - return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { + return JSONObject(httpGet(queryUrlB.build().toString(), ENCODING)).also { if (it.optInt("status") != 1) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, From 7688d7402d6db2367f59910d19cae8f8ef9bc5ac Mon Sep 17 00:00:00 2001 From: StefRe Date: Thu, 16 Jan 2020 23:14:01 +0100 Subject: [PATCH 13/55] Add remaining details to DetailedItem and fix reserved and lent items --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 73 +++++- .../opacclient/i18n/StringProvider.java | 5 + .../geeksfactory/opacclient/apis/SLUBTest.kt | 159 +++++++++++- .../resources/slub/search/item-fotothek.json | 43 ++++ .../resources/slub/search/item-links.json | 82 ++++++ .../search/item-links_general_no_label.json | 44 ++++ .../slub/search/item-multiple_parts_item.json | 76 ++++++ ...tem-with_umlaute_in_title_and_volumes.json | 233 ++++++++++++++++++ .../main/res/values-de/strings_api_errors.xml | 5 + .../main/res/values/strings_api_errors.xml | 5 + 10 files changed, 710 insertions(+), 15 deletions(-) create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-fotothek.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-links.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 68445fba3..2ec96b036 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -76,7 +76,10 @@ open class SLUB : OkHttpBaseApi() { "identifier" to "ISBN", "language" to "Sprache", "subject" to "Schlagwörter", - "description" to "Beschreibung" + "description" to "Beschreibung", + "linksRelated" to "Info", + "linksAccess" to "Zugang", + "linksGeneral" to "Link" ) override fun init(library: Library, factory: HttpClientFactory) { @@ -182,7 +185,10 @@ open class SLUB : OkHttpBaseApi() { val arrayItem = v.get(it) when (arrayItem) { is String -> arrayItem - is JSONObject -> arrayItem.optString("title") + is JSONObject -> arrayItem.optString("title").also { + // if item is part of multiple collections, collectionsId holds the last one + collectionId = arrayItem.optString(("id"), null) + } else -> null } }.joinToString("; ") @@ -196,6 +202,27 @@ open class SLUB : OkHttpBaseApi() { addDetail(Detail(fieldCaptions[key], value)) } } + json.optString("thumbnail")?.run { + if (this != "") { + cover = this + } + } + // links and references + for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ + val linkArray = json.optJSONArray(link) + linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map{ + // assuming that only on of material, note or hostlabel is set + val key = with(it.optString("material") + it.optString("note") + it.optString("hostLabel")) { + if (isEmpty()) fieldCaptions[link] else this + } + addDetail(Detail( key, it.optString("uri"))) + } + } + json.optJSONArray("references").run { 0.until(length()).map { optJSONObject(it) } }.map{ + // TODO: usually links to old SLUB catalogue, does it make sense to add the link? + addDetail(Detail( it.optString("text"), "${it.optString("name")} (${it.optString("target")})")) + } + // copies val cps = json.opt("copies") if (cps is JSONArray) { getCopies(cps, dateFormat)?.let { copies = it } @@ -209,10 +236,13 @@ open class SLUB : OkHttpBaseApi() { } copies = copiesList } - // TODO: volumes - // TODO: collectionid - // TODO: add linksAccess as detail (uri & hostLabel, note?, material?) - // TODO: add other links (links, linksRelated, linksGeneral) as details? + // volumes + volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { + 0.until(length()).map { optJSONObject(it) }?.map { + Volume(it.optString("id"), + "${it.optString("part")} ${Parser.unescapeEntities(it.optString("name"),false)}") + } + } ?: emptyList() } } @@ -253,6 +283,7 @@ open class SLUB : OkHttpBaseApi() { } internal fun parseAccountData(account: Account, json: JSONObject): AccountData { + val fmt = DateTimeFormat.shortDate() fun getReservations(items: JSONObject?): MutableList { val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" @@ -264,8 +295,25 @@ open class SLUB : OkHttpBaseApi() { ReservedItem().apply { title = it.optString("about") author = it.optJSONArray("X_author")?.optString(0) + //id = it.optString("label") // TODO: get details from here via /bc --> redirects to /id, from there get the proper id format = it.optString("X_medientyp") - status = it.optInt("X_queue_number").let { "Pos. $it" } + status = when(type){ // TODO: maybe we need time (LocalDateTime) too make an educated guess on actual ready date for stack requests + "hold" -> stringProvider.getFormattedString(StringProvider.HOLD, + fmt.print(LocalDate(it.optString("X_date_reserved").substring(0, 10)))) + "request_ready" -> stringProvider.getFormattedString(StringProvider.REQUEST_READY, + fmt.print(LocalDate(it.optString("X_date_requested").substring(0, 10)))) + "readingroom" -> stringProvider.getFormattedString(StringProvider.READINGROOM, + fmt.print(LocalDate(it.optString("X_date_provided").substring(0, 10)))) + "request_progress" -> stringProvider.getFormattedString(StringProvider.REQUEST_PROGRESS, + fmt.print(LocalDate(it.optString("X_date_requested").substring(0, 10)))) + "reserve" -> stringProvider.getFormattedString(StringProvider.RESERVED_POS, + it.optInt("X_queue_number")) + else -> null + } + branch = it.optString("X_pickup_desc", null) + if (type == "reserve") { + cancelData = "${it.optString("label")}_${it.getInt("X_delete_number")}" + } } }) } @@ -277,7 +325,10 @@ open class SLUB : OkHttpBaseApi() { ReservedItem().apply { title = it.optString("Titel") author = it.optString("Autor") - id = it.optString("Fernleih_ID") + //id = it.optString("Fernleih_ID") --> this id is of no use whatsoever + it.optString("Medientyp")?.run { + if (length > 0) format = this + } branch = it.optString("Zweigstelle") status = it.optString("Status_DESC") } @@ -288,12 +339,11 @@ open class SLUB : OkHttpBaseApi() { return reservationsList } - val fmt = DateTimeFormat.shortDate() return AccountData(account.id).apply { pendingFees = json.optJSONObject("fees")?.optString("topay_list") validUntil = json.optJSONObject("memberInfo")?.optString("expires") ?.substring(0, 10)?.let { fmt.print(LocalDate(it)) } - lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans (need example) + lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans? (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } ?.map { LentItem().apply { @@ -301,13 +351,14 @@ open class SLUB : OkHttpBaseApi() { author = it.optJSONArray("X_author")?.optString(0) setDeadline(it.optString("X_date_due")) format = it.optString("X_medientyp") + //id = it.optString("label") // TODO: get details from here via /bc --> redirects to /id, from there get the proper id barcode = it.optString("X_barcode") status = when { it.optInt("renewals") == 2 -> "2x verlängert" it.optInt("X_is_reserved") != 0 -> "vorgemerkt" else -> null } - isRenewable = if (it.optInt("X_is_renewable") == 1) { + isRenewable = if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items prolongData = barcode true } else { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java index 9feddbc08..395337f4e 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java @@ -85,6 +85,11 @@ public interface StringProvider { String DESCRIPTION = "description"; String RESERVATION_READY = "reservation_ready"; String PLEASE_CHANGE_PASSWORD = "please_change_password"; + String HOLD = "hold"; + String REQUEST_READY = "request_ready"; + String READINGROOM = "readingroom"; + String REQUEST_PROGRESS = "request_progress"; + String RESERVED_POS = "reserved_pos"; /** * Returns the translated string identified by identifier diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index c09e8d844..8b58f10df 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -1,20 +1,47 @@ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs +import de.geeksfactory.opacclient.i18n.StringProvider +import de.geeksfactory.opacclient.i18n.StringProvider.* import de.geeksfactory.opacclient.objects.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Test +private class TestStringProvider : StringProvider { + override fun getString(identifier: String?): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getFormattedString(identifier: String?, vararg args: Any?): String { + return when (identifier) { + RESERVED_POS -> String.format("vorgemerkt, Pos. %s", *args) + HOLD -> String.format("liegt seit %s bereit", *args) + REQUEST_READY -> String.format("seit %s abholbereit (Magazinbestellung)", *args) + else -> identifier!! + } + } + + override fun getQuantityString(identifier: String?, count: Int, vararg args: Any?): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getMediaTypeName(mediaType: SearchResult.MediaType?): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} class SLUBAccountTest() : BaseHtmlTest() { var slub = SLUB() + init { + slub.stringProvider = TestStringProvider() + } + @Test fun testParseEmptyAccountData() { val json = JSONObject(readResource("/slub/account/empty-account.json")) @@ -30,22 +57,52 @@ class SLUBAccountTest() : BaseHtmlTest() { @Test fun testParseAccountData() { val json = JSONObject(readResource("/slub/account/account.json")) + val fmt = DateTimeFormat.shortDate() val lentitem1 = LentItem().apply { title = "¬Der¬ neue Kosmos-Baumführer" author = "Bachofer, Mark" setDeadline("2019-06-03") format = "B" + //id = "31626878" barcode = "31626878" isRenewable = true prolongData = barcode } - + val reserveditem1 = ReservedItem().apply { + // reserve + title = "Pareys Buch der Bäume" + author = "Mitchell, Alan" + format = "B" + //id = "30963742" + status = "vorgemerkt, Pos. 1" + cancelData = "30963742_1" + } + val reserveditem2 = ReservedItem().apply { + // hold + title = "Welcher Baum ist das?" + author = "Mayer, Joachim ¬[VerfasserIn]¬" + format = "B" + //id = "34778398" + branch = "ZwB Forstwissenschaft" + status = String.format("liegt seit %s bereit", fmt.print(LocalDate("2019-05-10"))) + } + val reserveditem3 = ReservedItem().apply { + // request ready + title = "Englische Synonyme als Fehlerquellen" + author = "Meyer, Jürgen" + format = "B" + //id = "20550495" + branch = "Zentralbibliothek Ebene 0 SB-Regal" + status = String.format("seit %s abholbereit (Magazinbestellung)", fmt.print(LocalDate("2019-05-04"))) + } val accountdata = slub.parseAccountData(Account(), json) assertEquals(2, accountdata.lent.size) assertEquals(3, accountdata.reservations.size) assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) assertEquals("vorgemerkt", accountdata.lent[1].status) + assertThat(accountdata.reservations, hasItems(sameBeanAs(reserveditem1), + sameBeanAs(reserveditem2), sameBeanAs(reserveditem3))) } @Test @@ -54,7 +111,7 @@ class SLUBAccountTest() : BaseHtmlTest() { val reserveditem = ReservedItem().apply { title = "Kotlin" author = "Szwillus, Karl" - id = "145073" + //id = "145073" branch = "zell1" status = "Bestellung ausgelöst" } @@ -123,6 +180,7 @@ class SLUBSearchTest() : BaseHtmlTest() { addDetail(Detail("ISBN", "3446228241; 3446404694; 9783446404694; 9783446228245")) addDetail(Detail("Sprache", "Deutsch")) addDetail(Detail("Schlagwörter", "Quellcode; Softwaretest; JUnit")) + addDetail(Detail("Inhaltsverzeichnis", "http://d-nb.info/970689268/04")) id = "0-1182402208" copies = arrayListOf(Copy().apply { barcode = "31541466" @@ -131,6 +189,7 @@ class SLUBSearchTest() : BaseHtmlTest() { status = "Ausleihbar" shelfmark = "ST 233 H939" }) + collectionId = "0-1183957874" } val item = slub.parseResultById(json.getString("id"), json) @@ -164,4 +223,96 @@ class SLUBSearchTest() : BaseHtmlTest() { // the copies arrays may occur in any order assertThat(item.copies, hasItems(sameBeanAs(copyFirst), sameBeanAs(copyLast))) } + + @Test + fun testParseResultByIdMultipleParts() { + val json = JSONObject(readResource("/slub/search/item-multiple_parts_item.json")) + val volumes = listOf( + Volume("0-1453040935", "[3]: Principles of digital image processing"), + Volume("0-1347927328", "[2]: Principles of digital image processing"), + Volume("0-1347930884", "[1]: Principles of digital image processing") + ) + + // is part of "Undergraduate topics in computer science" but no id (--> collectionid) given + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(volumes, sameBeanAs(item.volumes)) + assertNull(item.collectionId) + } + + @Test + fun testParseResultByIdUmlaute() { + val json = JSONObject(readResource("/slub/search/item-with_umlaute_in_title_and_volumes.json")) + val volume = Volume("0-1149529121", "(inse,5): in 6 Bänden") + + val item = slub.parseResultById(json.getString("id"), json) + + assertEquals("Urania-Tierreich: in 6 Bänden", item.title) + assertThat(item.volumes, hasItem(sameBeanAs(volume))) + } + + @Test + fun testParseResultByIdThumbnail() { + val json = JSONObject(readResource("/slub/search/item-fotothek.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Foto")) + addDetail(Detail("Titel", "Maya")) + title = "Maya" + addDetail(Detail("Sprache", "Kein linguistischer Inhalt")) + addDetail(Detail("Schlagwörter", "Skulptur; Statue; Ortskatalog zur Kunst und Architektur")) + id = "dswarm-67-b2FpOmRldXRzY2hlZm90b3RoZWsuZGU6YTg0NTA6Om9ianwzMzA1NTgxMHxkZl9oYXVwdGthdGFsb2dfMDEwMDMzNg" + cover = "http://fotothek.slub-dresden.de/thumbs/df_hauptkatalog_0100336.jpg" + addDetail(Detail("In der Deutschen Fotothek ansehen", "http://www.deutschefotothek.de/obj33055810.html")) + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } + + @Test + fun testParseResultByIdLinks() { + val json = JSONObject(readResource("/slub/search/item-links.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Buch")) + addDetail(Detail("Titel", "JUnit-Profiwissen: effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java")) + title = "JUnit-Profiwissen: effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java" + addDetail(Detail("Beteiligte", "Tamm, Michael [Autor/In]")) + addDetail(Detail("Erschienen", "Heidelberg dpunkt.Verl. 2013 ")) + addDetail(Detail("ISBN", "3864900204; 9783864900204")) + addDetail(Detail("Sprache", "Deutsch")) + addDetail(Detail("Schlagwörter", "Java; JUnit")) + addDetail(Detail("Beschreibung", "Literaturverz. S. 351")) + id = "0-727434322" + addDetail(Detail("Inhaltsverzeichnis","http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf")) + addDetail(Detail("Inhaltstext","http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm")) + addDetail(Detail("Zugang zur Ressource (via ProQuest Ebook Central)", "http://wwwdb.dbod.de/login?url=http://slub.eblib.com/patron/FullRecord.aspx?p=1575685")) + addDetail(Detail("Online-Ausgabe", "Tamm, Michael: JUnit-Profiwissen (SLUB)")) + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } + + @Test + fun testParseResultByIdLinksGeneralNoLabel() { + val json = JSONObject(readResource("/slub/search/item-links_general_no_label.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "ElectronicThesis")) + addDetail(Detail("Titel", "A Study of Classic Maya Rulership")) + title = "A Study of Classic Maya Rulership" + addDetail(Detail("Sprache", "Englisch")) + addDetail(Detail("Schlagwörter", "Archaeology; Latin American history; Ancient history; Native American studies")) + id = "ai-34-b2FpOnBxZHRvYWkucHJvcXVlc3QuY29tOjM0Nzc2Mzg" + addDetail(Detail("Link", "http://pqdtopen.proquest.com/#viewpdf?dispub=3477638")) + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } } \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-fotothek.json b/opacclient/libopac/src/test/resources/slub/search/item-fotothek.json new file mode 100644 index 000000000..bcdcb7894 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-fotothek.json @@ -0,0 +1,43 @@ +{ + "record": { + "format": "Foto", + "title": "Maya", + "contributor": [], + "publisher": [], + "ispartof": [], + "identifier": "", + "language": [ + "Kein linguistischer Inhalt" + ], + "subject": [ + "Skulptur", + "Statue", + "Ortskatalog zur Kunst und Architektur" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "dswarm-67-b2FpOmRldXRzY2hlZm90b3RoZWsuZGU6YTg0NTA6Om9ianwzMzA1NTgxMHxkZl9oYXVwdGthdGFsb2dfMDEwMDMzNg", + "oa": 0, + "thumbnail": "http://fotothek.slub-dresden.de/thumbs/df_hauptkatalog_0100336.jpg", + "links": [ + { + "uri": "http://wwwdb.dbod.de/login?url=http://www.deutschefotothek.de/obj33055810.html", + "note": "In der Deutschen Fotothek ansehen" + } + ], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [ + { + "uri": "http://www.deutschefotothek.de/obj33055810.html", + "hostLabel": "In der Deutschen Fotothek ansehen", + "note": "", + "material": "" + } + ], + "references": [], + "copies": [], + "parts": {} +} diff --git a/opacclient/libopac/src/test/resources/slub/search/item-links.json b/opacclient/libopac/src/test/resources/slub/search/item-links.json new file mode 100644 index 000000000..960326916 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-links.json @@ -0,0 +1,82 @@ +{ + "record": { + "format": "Buch", + "title": "JUnit-Profiwissen: effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java", + "contributor": [ + "Tamm, Michael [Autor/In]" + ], + "publisher": [ + "Heidelberg dpunkt.Verl. 2013 " + ], + "ispartof": [], + "identifier": [ + "3864900204", + "9783864900204" + ], + "language": [ + "Deutsch" + ], + "subject": [ + "Java", + "JUnit" + ], + "description": [ + "Literaturverz. S. 351" + ], + "status": "", + "rvk": "" + }, + "id": "0-727434322", + "oa": 0, + "thumbnail": "", + "links": [ + { + "uri": "http://slub.eblib.com/patron/FullRecord.aspx?p=1575685", + "note": "Zugang zur Ressource (via ProQuest Ebook Central)", + "material": "" + }, + { + "uri": "http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf", + "note": "Auf PDF zugreifen", + "material": "Inhaltsverzeichnis" + }, + { + "uri": "http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm", + "note": "", + "material": "Inhaltstext" + } + ], + "linksRelated": [ + { + "uri": "http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf", + "hostLabel": "", + "note": "", + "material": "Inhaltsverzeichnis" + }, + { + "uri": "http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm", + "hostLabel": "", + "note": "", + "material": "Inhaltstext" + } + ], + "linksAccess": [ + { + "uri": "http://wwwdb.dbod.de/login?url=http://slub.eblib.com/patron/FullRecord.aspx?p=1575685", + "hostLabel": "Zugang zur Ressource (via ProQuest Ebook Central)", + "note": "", + "material": "" + } + ], + "linksGeneral": [], + "references": [ + { + "text": "Online-Ausgabe", + "link": "http://slubdd.de/katalog?libero_mab216187885", + "name": "Tamm, Michael: JUnit-Profiwissen", + "target": "SLUB" + } + ], + "copies": [], + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json b/opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json new file mode 100644 index 000000000..ecdaca72c --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json @@ -0,0 +1,44 @@ +{ + "record": { + "format": "ElectronicThesis", + "title": "A Study of Classic Maya Rulership", + "contributor": [], + "publisher": [], + "ispartof": [], + "identifier": "", + "language": [ + "Englisch" + ], + "subject": [ + "Archaeology", + "Latin American history", + "Ancient history", + "Native American studies" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "ai-34-b2FpOnBxZHRvYWkucHJvcXVlc3QuY29tOjM0Nzc2Mzg", + "oa": 0, + "thumbnail": "", + "links": [ + { + "uri": "http://wwwdb.dbod.de/login?url=http://pqdtopen.proquest.com/#viewpdf?dispub=3477638", + "note": "http://wwwdb.dbod.de/login?url=http://pqdtopen.proquest.com/#viewpdf?dispub=3477638" + } + ], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [ + { + "uri": "http://pqdtopen.proquest.com/#viewpdf?dispub=3477638", + "hostLabel": "", + "note": "", + "material": "" + } + ], + "references": [], + "copies": [], + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json b/opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json new file mode 100644 index 000000000..3307283d4 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json @@ -0,0 +1,76 @@ +{ + "record": { + "format": "Buch", + "title": "Principles of digital image processing", + "contributor": [ + "Burger, Wilhelm", + "Burge, Mark James [Autor/In]" + ], + "publisher": [ + "London Springer 20XX " + ], + "ispartof": [ + { + "title": "Undergraduate topics in computer science" + } + ], + "identifier": "", + "language": [ + "Englisch" + ], + "subject": "", + "description": "", + "status": "", + "rvk": "" + }, + "id": "0-1393168353", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [], + "parts": { + "title": "Mehrbändiges Werk", + "records": [ + { + "author": [ + "Burger, Wilhelm", + "Burge, Mark J." + ], + "author2": [], + "part": "[3]:", + "id": "0-1453040935", + "name": "Principles of digital image processing", + "imprint": "London, Springer, 2013", + "sequence": "[3]" + }, + { + "author": [ + "Burger, Wilhelm", + "Burge, Mark James" + ], + "author2": [], + "part": "[2]:", + "id": "0-1347927328", + "name": "Principles of digital image processing", + "imprint": "London, Springer, 2009", + "sequence": "[2]" + }, + { + "author": [ + "Burger, Wilhelm", + "Burge, Mark James" + ], + "author2": [], + "part": "[1]:", + "id": "0-1347930884", + "name": "Principles of digital image processing", + "imprint": "London, Springer, 2009", + "sequence": "[1]" + } + ] + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json b/opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json new file mode 100644 index 000000000..bdc76f97f --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json @@ -0,0 +1,233 @@ +{ + "record": { + "format": "Buch", + "title": "Urania-Tierreich: in 6 Bänden", + "contributor": [], + "publisher": [ + "Leipzig Jena Berlin Urania-Verl. 19XX- " + ], + "ispartof": [], + "identifier": "", + "language": [ + "Deutsch" + ], + "subject": [ + "Tiere" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "0-112458708X", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [], + "parts": { + "title": "Mehrbändiges Werk", + "records": [ + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse,5):", + "id": "0-1149529121", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1989", + "sequence": "inse,5" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": ":", + "id": "0-1124595643", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1986", + "sequence": "saug,5" + }, + { + "author": [], + "author2": [ + "Gruner, Hans-Eckhard" + ], + "part": "(wirb,1,3):", + "id": "0-1149528753", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1981", + "sequence": "wirb,1,3" + }, + { + "author": [], + "author2": [ + "Crome, Wolfgang" + ], + "part": "(wirb,2,3):", + "id": "0-1149528885", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1979", + "sequence": "wirb,2,3" + }, + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse,4):", + "id": "0-1162094052", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1978", + "sequence": "inse,4" + }, + { + "author": [], + "author2": [ + "Mauersberger, Gottfried" + ], + "part": "(voge):", + "id": "0-1129107655", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1977", + "sequence": "voge" + }, + { + "author": [], + "author2": [ + "Deckert, Kurt" + ], + "part": "(fisc,4):", + "id": "0-1149529466", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1976", + "sequence": "fisc,4" + }, + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse, 2./3.):", + "id": "0-1124590285", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1974", + "sequence": "inse, 2./3." + }, + { + "author": [], + "author2": [ + "Deckert, Kurt" + ], + "part": "(fisc,3):", + "id": "0-1162153717", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1973", + "sequence": "fisc,3" + }, + { + "author": [], + "author2": [ + "Mauersberger, Gottfried" + ], + "part": "(voge,3):", + "id": "0-1162188383", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1972", + "sequence": "voge,3" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": "(saug,3):", + "id": "0-112459082X", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1972", + "sequence": "saug,3" + }, + { + "author": [], + "author2": [ + "Mauersberger, Gottfried" + ], + "part": "(voge):", + "id": "0-1124587314", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1969", + "sequence": "voge" + }, + { + "author": [], + "author2": [ + "Crome, Wolfgang" + ], + "part": "(wirb,2):", + "id": "0-1124588604", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1969", + "sequence": "wirb,2" + }, + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse):", + "id": "0-1124590048", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1968", + "sequence": "inse" + }, + { + "author": [], + "author2": [ + "Deckert, Kurt" + ], + "part": "(fisc):", + "id": "0-1124587748", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1967", + "sequence": "fisc" + }, + { + "author": [], + "author2": [ + "Crome, Wolfgang" + ], + "part": "(wirb,1):", + "id": "0-112458983X", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1967", + "sequence": "wirb,1" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": "(saug):", + "id": "0-1124591036", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1967", + "sequence": "saug" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": "(saug,1):", + "id": "0-117225883X", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1966", + "sequence": "saug,1" + } + ] + } +} diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index 7ffaf8cbe..5d120fdfc 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -85,4 +85,9 @@ Beschreibung Abholbereit Die Bibliothek fordert Sie auf, Ihr Passwort zu ändern. Bitte loggen Sie sich dazu auf der Bibliothekswebsite ein, um das Passwort zu ändern, und geben dann das neue Passwort in der App ein. + liegt seit %s bereit + seit %s abholbereit (Magazinbestellung) + liegt seit %s zur Benutzung in Bibliothek bereit + Magazinbestellung seit %s in Arbeit + vorgemerkt, Pos. %s diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index a37de0f1e..42363136f 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -86,4 +86,9 @@ Description Ready for pickup The library is asking you to change your password. Please first log in on the library website to change your password, and then enter the new password in the app. + on hold since %s + ready for pickup since %s (stack request) + ready since %s for usage in library + stack request in progress since %s + reserved, pos. %s From 7ed81399a167bdf1ec106fe709da046b447a001a Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 19 Jan 2020 19:10:38 +0100 Subject: [PATCH 14/55] Add SUPPORT_FLAG_ENDLESS_SCROLLING flag to prevent reloading the page the selected result is located on before calling getResultById. --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 2ec96b036..1051d7b9f 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -403,7 +403,7 @@ open class SLUB : OkHttpBaseApi() { } override fun getSupportFlags(): Int { - return 0 + return OpacApi.SUPPORT_FLAG_ENDLESS_SCROLLING } override fun getSupportedLanguages(): Set? { From 71b77d25e4e3ae8a2d8b8e21c0d3eb83a25aacfe Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 19 Jan 2020 20:02:32 +0100 Subject: [PATCH 15/55] Partly revert d18931b32d337cfeff279427e175c0c13bd0aea5 As of Jan 17, 2020, POST requests are accepted again. So we change the account requests back to POST for security reasons (see recommendation https://tools.ietf.org/html/rfc2616#section-15.1.3) --- .../java/de/geeksfactory/opacclient/apis/SLUB.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 1051d7b9f..ab2191528 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -30,6 +30,7 @@ import de.geeksfactory.opacclient.searchfields.TextSearchField import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text +import okhttp3.FormBody import okhttp3.HttpUrl import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat @@ -371,16 +372,17 @@ open class SLUB : OkHttpBaseApi() { } private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { - val queryUrlB = HttpUrl.get("$baseurl/mein-konto/?type=1&tx_slubaccount_account[controller]=API") - .newBuilder() - .addQueryParameter("tx_slubaccount_account[action]", action) - .addQueryParameter("tx_slubaccount_account[username]", account.name) - .addQueryParameter("tx_slubaccount_account[password]", account.password) + val formBody = FormBody.Builder() + .add("type", "1") + .add("tx_slubaccount_account[controller]", "API") + .add("tx_slubaccount_account[action]", action) + .add("tx_slubaccount_account[username]", account.name) + .add("tx_slubaccount_account[password]", account.password) parameters?.map { - queryUrlB.addQueryParameter(it.key, it.value) + formBody.add(it.key, it.value) } try { - return JSONObject(httpGet(queryUrlB.build().toString(), ENCODING)).also { + return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { if (it.optInt("status") != 1) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, From a29ba20c1e8e7bca205e5bc3b6716dc1a5e0e202 Mon Sep 17 00:00:00 2001 From: StefRe Date: Thu, 23 Jan 2020 22:49:28 +0100 Subject: [PATCH 16/55] Process boolean status values in account request results and add test for requestAccount --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 4 +- .../geeksfactory/opacclient/apis/SLUBTest.kt | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index ab2191528..24e0d5653 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -371,7 +371,7 @@ open class SLUB : OkHttpBaseApi() { } } - private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { + internal fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { val formBody = FormBody.Builder() .add("type", "1") .add("tx_slubaccount_account[controller]", "API") @@ -383,7 +383,7 @@ open class SLUB : OkHttpBaseApi() { } try { return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { - if (it.optInt("status") != 1) { + if (!(it.optInt("status") == 1 || it.optBoolean("status"))) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, it.optString("message", "error requesting account data"))) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 8b58f10df..6afa6af6d 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -3,6 +3,7 @@ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.i18n.StringProvider.* +import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* @@ -10,7 +11,13 @@ import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.json.JSONObject import org.junit.Assert.* +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Matchers +import org.mockito.Mockito private class TestStringProvider : StringProvider { override fun getString(identifier: String?): String { @@ -315,4 +322,55 @@ class SLUBSearchTest() : BaseHtmlTest() { assertThat(item, sameBeanAs(expected).ignoring("details")) assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) } +} + +@RunWith(Parameterized::class) +class SLUBAccountMockTest(private val response: String, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + } + }, HttpClientFactory("test")) + } + private val account = Account().apply { + name = "x" + password = "x" + } + + @JvmField + @Rule + var thrown: ExpectedException = ExpectedException.none() + + @Test + fun testCheckAccountData() { + Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) + if (expectedException != null) { + thrown.expect(expectedException) + thrown.expectMessage(expectedExceptionMsg) + } + + slub.requestAccount(account, "", null) + } + + companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = listOf( + // validate: status as string + arrayOf("{\"status\":\"1\",\"message\":\"credentials_are_valid\"}", null, null), + arrayOf("{\"message\":\"error_credentials_invalid\",\"arguments\":{\"controller\":\"API\",\"action\":\"validate\",\"username\":\"123456\"},\"status\":\"-1\"}", OpacApi.OpacErrorException::class.java, "error_credentials_invalid"), + // POST not accepted, malformed request, e.g. invalid action + arrayOf(".", OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), + // delete: status as int or string + arrayOf("{\"status\":1,\"message\":\"Reservation deleted\"}", null, null), + arrayOf("{\"status\":\"-1\",\"message\":\"Item not reserved\"}", OpacApi.OpacErrorException::class.java, "Item not reserved"), + // pickup: status as boolean + arrayOf("{\"status\":true,\"message\":\"n\\/a\"}", null, null), + arrayOf("{\"status\":false,\"message\":\"Ungültige Barcodenummer\"}", OpacApi.OpacErrorException::class.java, "Ungültige Barcodenummer") + ) + } } \ No newline at end of file From 158c1540b7e701c67a0042b2a511fb0e724207d9 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 29 Jan 2020 21:18:59 +0100 Subject: [PATCH 17/55] Implement reservation and tests for it - minor fix to SLUBAccountMockTest - combine all SLUB tests into test suite --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 115 +++++- .../opacclient/i18n/StringProvider.java | 1 + .../geeksfactory/opacclient/apis/SLUBTest.kt | 369 +++++++++++++++++- .../slub/search/item-for-reserve&request.json | 98 +++++ .../main/res/values-de/strings_api_errors.xml | 1 + .../main/res/values/strings_api_errors.xml | 1 + 6 files changed, 570 insertions(+), 15 deletions(-) create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 24e0d5653..a3b9dc9a5 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -83,6 +83,21 @@ open class SLUB : OkHttpBaseApi() { "linksGeneral" to "Link" ) + private val ACTION_COPY = OpacApi.MultiStepResult.ACTION_USER + 1 + private val pickupPoints = mapOf( + "a01" to "Zentralbibliothek Ebene 0 SB-Regal", + "a02" to "Zentralbibliothek, Ebene -1, IP Musik Mediathek", + "a03" to "Zentralbibliothek, Ebene -1, Lesesaal Sondersammlungen", + "a05" to "Zentralbibliothek, Ebene -2, Lesesaal Kartensammlung", + "a06" to "ZwB Rechtswissenschaft", + "a07" to "ZwB Erziehungswissenschaft", + "a08" to "ZwB Medizin", + "a09" to "ZwB Forstwissenschaft", + "a10" to "Bereichsbibliothek Drepunct", + "a13" to "Zentralbibliothek, Servicetheke", + "a14" to "Zentralbibliothek, Ebene -1, SB-Regal Zeitungen" + ) + override fun init(library: Library, factory: HttpClientFactory) { super.init(library, factory) baseurl = library.data.getString("baseurl") @@ -153,6 +168,7 @@ open class SLUB : OkHttpBaseApi() { internal fun parseResultById(id:String, json: JSONObject): DetailedItem { val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") + var hasReservableCopies = false fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = copiesArray.run { 0.until(length()).map { optJSONObject(it) } } .map { @@ -167,8 +183,14 @@ open class SLUB : OkHttpBaseApi() { returnDate = df.parseLocalDate(this) } } + // stack requests and reservations for items on loan are both handled as "reservations" in libopac + if (it.getString("bestellen") == "1") { + resInfo = "stackRequest\t$barcode" + hasReservableCopies = true + } if (it.getString("vormerken") == "1") { - resInfo = barcode + resInfo = "reserve\t$barcode" + hasReservableCopies = true } // reservations: only available for reserved copies, not for reservable copies // url: not for accessible online resources, only for lendable online copies @@ -237,6 +259,7 @@ open class SLUB : OkHttpBaseApi() { } copies = copiesList } + isReservable = hasReservableCopies // volumes volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { 0.until(length()).map { optJSONObject(it) }?.map { @@ -253,7 +276,95 @@ open class SLUB : OkHttpBaseApi() { } override fun reservation(item: DetailedItem, account: Account, useraction: Int, selection: String?): OpacApi.ReservationResult { - TODO("not implemented") + var action = useraction + var selected = selection ?: "" + // step 1: select copy to request/reserve if there are multiple requestable/reservable copies + // if there's just one requestable/reservable copy then go to step 2 + if (action == 0 && selection == null) { + val reservableCopies = item.copies.filter { it.resInfo != null } + when (reservableCopies.size) { + 0 -> return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.NO_COPY_RESERVABLE)) + 1 -> { + action = ACTION_COPY + selected = reservableCopies.first().resInfo + } + else -> { + val options = reservableCopies.map { copy -> + mapOf("key" to copy.resInfo, + "value" to "${copy.branch}: ${copy.status}") + } + return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, + stringProvider.getString(StringProvider.COPY)).apply { + actionIdentifier = ACTION_COPY + this.selection = options + } + } + } + } + // step 2: select pickup branch (reservations) or pickup point (stack requests) + // if there's just one pickup point then go to step 3 + if (action == ACTION_COPY) { + val pickupLocations: Map + if (selected.startsWith("reserve")) { + pickupLocations = mapOf( + "zell1" to "Zentralbibliothek", + "bebel1" to "ZwB Erziehungswissenschaften", + "berg1" to "ZwB Rechtswissenschaft", + "fied1" to "ZwB Medizin", + "tha1" to "ZwB Forstwissenschaft", + "zell9" to "Bereichsbibliothek Drepunct" + ) + } else { + val data = selected.split('\t') + if (data.size != 2) { + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.INTERNAL_ERROR)) + } + try { + val json = requestAccount(account, "pickup", mapOf("tx_slubaccount_account[barcode]" to data[1])) + pickupLocations = json.optJSONArray("PickupPoints")?.run { + 0.until(length()).map { optString(it) }.map { + it to pickupPoints.getOrElse(it){it} + } + }?.toMap() ?: emptyMap() + } catch (e: OpacApi.OpacErrorException) { + return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } + } + when (pickupLocations.size) { + 0 -> return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.INTERNAL_ERROR)) + 1 -> { + action = OpacApi.ReservationResult.ACTION_BRANCH + selected += "\t${pickupLocations.keys.first()}" + } + else -> return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = pickupLocations.map { + mapOf("key" to "$selected\t${it.key}", + "value" to it.value) + } + } + } + } + // step 3. make stack request or reservation + if (action == OpacApi.ReservationResult.ACTION_BRANCH) { + val data = selected.split('\t') + if (data.size == 3) { + val pickupParameter = if (data[0] == "stackRequest") "pickupPoint" else "PickupBranch" + return try { + val json = requestAccount(account, data[0], + mapOf("tx_slubaccount_account[barcode]" to data[1], + "tx_slubaccount_account[$pickupParameter]" to data[2])) + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, json.optString("message")) + } catch (e: OpacApi.OpacErrorException) { + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } + } + } + return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.INTERNAL_ERROR)) } override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java index 395337f4e..285af62c5 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java @@ -90,6 +90,7 @@ public interface StringProvider { String READINGROOM = "readingroom"; String REQUEST_PROGRESS = "request_progress"; String RESERVED_POS = "reserved_pos"; + String COPY = "copy"; /** * Returns the translated string identified by identifier diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 6afa6af6d..3313f20af 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -1,3 +1,24 @@ +/* + * Copyright (C) 2020 by Steffen Rehberg under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs @@ -5,6 +26,8 @@ import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.i18n.StringProvider.* import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* +import okhttp3.FormBody +import okhttp3.RequestBody import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* import org.joda.time.LocalDate @@ -16,8 +39,28 @@ import org.junit.Test import org.junit.rules.ExpectedException import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.junit.runners.Suite +import org.mockito.AdditionalMatchers.or +import org.mockito.ArgumentMatcher import org.mockito.Matchers +import org.mockito.Matchers.argThat import org.mockito.Mockito +import org.mockito.Mockito.verify + +/** + * Tests for SLUB API + * + * @author Steffen Rehberg, Jan 2020 + */ +@RunWith(Suite::class) +@Suite.SuiteClasses( + SLUBAccountTest::class, + SLUBSearchTest::class, + SLUBAccountMockTest::class, + SLUBReservationMockTest::class, + SLUBAccountValidateMockTest::class +) +class SLUBAllTests private class TestStringProvider : StringProvider { override fun getString(identifier: String?): String { @@ -215,6 +258,7 @@ class SLUBSearchTest() : BaseHtmlTest() { branch = "Zentralbibliothek" status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" shelfmark = "19 4 01339 0 0024 1 01" + resInfo = "stackRequest\t10418078" } val copyLast = Copy().apply { barcode = "33364639" @@ -222,6 +266,7 @@ class SLUBSearchTest() : BaseHtmlTest() { branch = "Zentralbibliothek" status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" shelfmark = "19 4 01339 1 1969 1 01" + resInfo = "stackRequest\t33364639" } val item = slub.parseResultById(json.getString("id"), json) @@ -322,13 +367,66 @@ class SLUBSearchTest() : BaseHtmlTest() { assertThat(item, sameBeanAs(expected).ignoring("details")) assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) } + + @Test + fun testParseResultByIdResinfo() { + val json = JSONObject(readResource("/slub/search/item-for-reserve&request.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Buch")) + addDetail(Detail("Titel", "Der Fürstenzug zu Dresden: Denkmal und Geschichte des Hauses Wettin")) + title = "Der Fürstenzug zu Dresden: Denkmal und Geschichte des Hauses Wettin" + addDetail(Detail("Beteiligte", "Blaschke, Karlheinz [Autor/In]; Beyer, Klaus G. [Ill.]")) + addDetail(Detail("Erschienen", "Leipzig Jena Berlin Urania-Verl. 1991 ")) + addDetail(Detail("ISBN", "3332003771; 9783332003772")) + addDetail(Detail("Sprache", "Deutsch")) + addDetail(Detail("Schlagwörter", "Walther, Wilhelm; Albertiner; Albertiner; Fries; Walther, Wilhelm")) + addDetail(Detail("Beschreibung", "Literaturverz. S. 222 - 224")) + id = "0-276023927" + copies = arrayListOf( + Copy().apply { + barcode = "10059731" + department = "Freihand" + branch = "Zentralbibliothek" + status = "Ausgeliehen, Vormerken möglich" + shelfmark = "LK 24099 B644" + returnDate = LocalDate(2020, 2, 5) + resInfo = "reserve\t10059731" + }, + + Copy().apply { + barcode = "30523028" + department = "Freihand" + branch = "ZwB Erziehungswissenschaften" + status = "Benutzung nur im Haus, Versand per Fernleihe möglich" + shelfmark = "NR 6400 B644 F9" + }, + Copy().apply { + barcode = "20065307" + department = "Magazin" + branch = "Zentralbibliothek" + status = "Ausleihbar, bitte bestellen" + shelfmark = "65.4.653.b" + resInfo = "stackRequest\t20065307" + } + ) + isReservable = true + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } } @RunWith(Parameterized::class) -class SLUBAccountMockTest(private val response: String, - private val expectedException: Class?, - private val expectedExceptionMsg: String?) : BaseHtmlTest() { +class SLUBAccountMockTest(@Suppress("unused") private val name: String, + private val response: String, + private val expectedMessage: String?, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { private val slub = Mockito.spy(SLUB::class.java) + init { slub.init(Library().apply { data = JSONObject().apply { @@ -336,6 +434,7 @@ class SLUBAccountMockTest(private val response: String, } }, HttpClientFactory("test")) } + private val account = Account().apply { name = "x" password = "x" @@ -353,24 +452,268 @@ class SLUBAccountMockTest(private val response: String, thrown.expectMessage(expectedExceptionMsg) } - slub.requestAccount(account, "", null) + val actual = slub.requestAccount(account, "", null) + assertEquals(expectedMessage, actual.optString("message")) } companion object { @JvmStatic - @Parameterized.Parameters + @Parameterized.Parameters(name = "{0}") fun data() = listOf( // validate: status as string - arrayOf("{\"status\":\"1\",\"message\":\"credentials_are_valid\"}", null, null), - arrayOf("{\"message\":\"error_credentials_invalid\",\"arguments\":{\"controller\":\"API\",\"action\":\"validate\",\"username\":\"123456\"},\"status\":\"-1\"}", OpacApi.OpacErrorException::class.java, "error_credentials_invalid"), + arrayOf("String - OK", "{\"status\":\"1\",\"message\":\"credentials_are_valid\"}", "credentials_are_valid", null, null), + arrayOf("String - Error", "{\"message\":\"error_credentials_invalid\",\"arguments\":{\"controller\":\"API\",\"action\":\"validate\",\"username\":\"123456\"},\"status\":\"-1\"}", null, OpacApi.OpacErrorException::class.java, "error_credentials_invalid"), // POST not accepted, malformed request, e.g. invalid action - arrayOf(".", OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), + arrayOf("Malformed", ".", null, OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), // delete: status as int or string - arrayOf("{\"status\":1,\"message\":\"Reservation deleted\"}", null, null), - arrayOf("{\"status\":\"-1\",\"message\":\"Item not reserved\"}", OpacApi.OpacErrorException::class.java, "Item not reserved"), + arrayOf("Int/string - OK", "{\"status\":1,\"message\":\"Reservation deleted\"}", "Reservation deleted", null, null), + arrayOf("Int/string - Error", "{\"status\":\"-1\",\"message\":\"Item not reserved\"}", null, OpacApi.OpacErrorException::class.java, "Item not reserved"), // pickup: status as boolean - arrayOf("{\"status\":true,\"message\":\"n\\/a\"}", null, null), - arrayOf("{\"status\":false,\"message\":\"Ungültige Barcodenummer\"}", OpacApi.OpacErrorException::class.java, "Ungültige Barcodenummer") + arrayOf("Boolean - OK", "{\"status\":true,\"message\":\"n\\/a\"}", "n/a", null, null), + arrayOf("Boolean - Error", "{\"status\":false,\"message\":\"Ungültige Barcodenummer\"}", null, OpacApi.OpacErrorException::class.java, "Ungültige Barcodenummer") ) } -} \ No newline at end of file +} + +@RunWith(Parameterized::class) +class SLUBReservationMockTest(@Suppress("unused") private val name: String, + private val item: DetailedItem, + private val useraction: Int, + private val selection: String?, + private val responsePickup: String?, + private val responseReserveOrRequest: String?, + private val expectedResult: OpacApi.ReservationResult) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "123456" + password = "x" + } + + @Test + fun testReservation() { + if (responsePickup != null) { + Mockito.doReturn(responsePickup).`when`(slub).httpPost(Matchers.any(), + argThat(IsRequestBodyWithAction("pickup")), Matchers.any()) + } + if (responseReserveOrRequest != null) { + Mockito.doReturn(responseReserveOrRequest).`when`(slub).httpPost(Matchers.any(), + or(argThat(IsRequestBodyWithAction("stackRequest")), + argThat(IsRequestBodyWithAction("reserve"))), Matchers.any()) + } + + val result = slub.reservation(item, account, useraction, selection) + assertThat(result, sameBeanAs(expectedResult)) + } + + companion object { + // this item has already been tested in testParseResultByIdResinfo so we can rely on parseResultById here + private val json = JSONObject(BaseHtmlTest().readResource("/slub/search/item-for-reserve&request.json")) + private val itemRequestAndReserve = SLUB().parseResultById(json.getString("id"), json) + private val itemRequest = SLUB().parseResultById(json.getString("id"), json) + private val itemReserve = SLUB().parseResultById(json.getString("id"), json) + private val itemNone = SLUB().parseResultById(json.getString("id"), json) + + init { + itemRequest.apply { + copies = copies.filter { + it.resInfo?.startsWith("stackRequest") ?: false + } + } + itemReserve.apply { + copies = copies.filter { + it.resInfo?.startsWith("reserve") ?: false + } + } + itemNone.apply { copies = copies.filter { it.resInfo == null } } + } + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = listOf( + arrayOf("Single reservable copy (with multiple pickup branches)", + itemReserve, + 0, + null, + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "reserve\t10059731\tzell1", "value" to "Zentralbibliothek"), + mapOf("key" to "reserve\t10059731\tbebel1", "value" to "ZwB Erziehungswissenschaften"), + mapOf("key" to "reserve\t10059731\tberg1", "value" to "ZwB Rechtswissenschaft"), + mapOf("key" to "reserve\t10059731\tfied1", "value" to "ZwB Medizin"), + mapOf("key" to "reserve\t10059731\ttha1", "value" to "ZwB Forstwissenschaft"), + mapOf("key" to "reserve\t10059731\tzell9", "value" to "Bereichsbibliothek Drepunct") + ) + } + ), + arrayOf("Make reservation (for selected pickup branch)", + itemReserve, + OpacApi.ReservationResult.ACTION_BRANCH, + "reserve\t10059731\tbebel1", + null, + "{\"status\":1,\"message\":\"Ihre Vormerkung wurde vorgenommen|Diese Vormerkung läuft ab am 23 Apr 2020|Position in der Vormerkerliste 1\",\"arguments\":{\"controller\":\"API\",\"action\":\"reserve\",\"barcode\":\"10059731\",\"username\":\"123456\",\"PickupBranch\":\"zell1\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Ihre Vormerkung wurde vorgenommen|Diese Vormerkung läuft ab am 23 Apr 2020|Position in der Vormerkerliste 1") + ), + arrayOf("Single requestable copy with single pickup point", + itemRequest, + 0, + null, + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") + ), + arrayOf("Single requestable copy with multiple pickup points", + itemRequest, + 0, + null, + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\",\"a13\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "stackRequest\t20065307\ta01", "value" to "Zentralbibliothek Ebene 0 SB-Regal"), + mapOf("key" to "stackRequest\t20065307\ta13", "value" to "Zentralbibliothek, Servicetheke") + ) + } + ), + arrayOf("Single requestable copy with multiple pickup points including unknown ones", + itemRequest, + 0, + null, + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\",\"xxx\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "stackRequest\t20065307\ta01", "value" to "Zentralbibliothek Ebene 0 SB-Regal"), + mapOf("key" to "stackRequest\t20065307\txxx", "value" to "xxx") + ) + } + ), + arrayOf("Make stack request for selected pickup point", + itemRequest, + OpacApi.ReservationResult.ACTION_BRANCH, + "stackRequest\t20065307\ta01", + null, + "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") + ), + arrayOf("Multiple requestable or reservable copies", + itemRequestAndReserve, + 0, + null, + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, "copy").apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_USER + 1 + this.selection = listOf( + mapOf("key" to "reserve\t10059731", "value" to "Zentralbibliothek: Ausgeliehen, Vormerken möglich"), + mapOf("key" to "stackRequest\t20065307", "value" to "Zentralbibliothek: Ausleihbar, bitte bestellen")) + } + ), + arrayOf("Selected reservable copy (with multiple pickup branches)", + itemRequestAndReserve, + OpacApi.ReservationResult.ACTION_USER + 1, // == ACTION_COPY + "reserve\t10059731", + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "reserve\t10059731\tzell1", "value" to "Zentralbibliothek"), + mapOf("key" to "reserve\t10059731\tbebel1", "value" to "ZwB Erziehungswissenschaften"), + mapOf("key" to "reserve\t10059731\tberg1", "value" to "ZwB Rechtswissenschaft"), + mapOf("key" to "reserve\t10059731\tfied1", "value" to "ZwB Medizin"), + mapOf("key" to "reserve\t10059731\ttha1", "value" to "ZwB Forstwissenschaft"), + mapOf("key" to "reserve\t10059731\tzell9", "value" to "Bereichsbibliothek Drepunct") + ) + } + ), + arrayOf("Selected requestable copy with single pickup point", + itemRequestAndReserve, + OpacApi.ReservationResult.ACTION_USER + 1, // == ACTION_COPY + "stackRequest\t20065307", + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") + ), + // "selected requestable copy wiht multiple pickup ponits" doesn't need to be tested as it's the same process as "Selected reservable copy (with multiple pickup branches)" + arrayOf("No requestable or reservable copies", + itemNone, + 0, + null, + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, "no_copy_reservable") + ), + arrayOf("Error getting pickup points", + itemRequest, + 0, + null, + "", + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, "unknown_error_account_with_description accountRequest didn't return JSON object: A JSONObject text must begin with '{' at character 0") + ) + ) + } +} + +class SLUBAccountValidateMockTest : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "test") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "x" + password = "x" + } + + @Test + fun testCheckAccountData() { + val response = "{\"status\":\"1\",\"message\":\"credentials_are_valid\"}" + Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) + + slub.checkAccountData(account) + verify(slub).httpPost(Matchers.any(), argThat(IsRequestBodyWithAction("validate")), Matchers.any()) + } +} + +class IsRequestBodyWithAction(private val action: String) : ArgumentMatcher() { + override fun matches(arg: Any ): Boolean { + val fb = arg as FormBody? + for(i in 0 until (fb?.size() ?: 0)){ + if (fb!!.value(i) == action) + return true + } + return false + } +} + +class IsRequestBodyWithActionTest { + val fb: FormBody = FormBody.Builder().add("name", "value").build() + @Test + fun `matcher matches`() = assertTrue(IsRequestBodyWithAction("value").matches(fb)) + + @Test + fun `matcher doesn't match`() = assertFalse(IsRequestBodyWithAction("").matches(fb)) + + @Test + fun `matcher doesn't match empty formbody`() = assertFalse(IsRequestBodyWithAction("").matches(FormBody.Builder().build())) +} diff --git a/opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json b/opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json new file mode 100644 index 000000000..9e35cf511 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json @@ -0,0 +1,98 @@ +{ + "record": { + "format": "Buch", + "title": "Der Fürstenzug zu Dresden: Denkmal und Geschichte des Hauses Wettin", + "contributor": [ + "Blaschke, Karlheinz [Autor/In]", + "Beyer, Klaus G. [Ill.]" + ], + "publisher": [ + "Leipzig Jena Berlin Urania-Verl. 1991 " + ], + "ispartof": [], + "identifier": [ + "3332003771", + "9783332003772" + ], + "language": [ + "Deutsch" + ], + "subject": [ + "Walther, Wilhelm", + "Albertiner", + "Albertiner", + "Fries", + "Walther, Wilhelm" + ], + "description": [ + "Literaturverz. S. 222 - 224" + ], + "status": "", + "rvk": "" + }, + "id": "0-276023927", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [ + { + "barcode": "10059731", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Freihand", + "shelfmark": "LK 24099 B644", + "mediatype": "B", + "3d": "LK 24099 B644", + "3d_link": "https://3d.slub-dresden.de/viewer?project_id=3&search_key=LK%2024099%20B644&language=de&search_context1=zell1&search_context2=FH1&exemplar_id=10059731", + "issue": "", + "colorcode": "3", + "statusphrase": "Ausgeliehen, Vormerken möglich", + "link": "", + "status": "N", + "duedate": "05.02.2020", + "vormerken": "1", + "bestellen": "0" + }, + { + "barcode": "30523028", + "location": "ZwB Erziehungswissenschaften", + "location_code": "bebel1", + "sublocation": "Freihand", + "shelfmark": "NR 6400 B644 F9", + "mediatype": "B", + "3d": "", + "3d_link": "", + "issue": "", + "colorcode": "1", + "statusphrase": "Benutzung nur im Haus, Versand per Fernleihe möglich", + "link": "", + "status": "P1", + "duedate": "", + "vormerken": "0", + "bestellen": "0" + }, + { + "barcode": "20065307", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin", + "shelfmark": "65.4.653.b", + "mediatype": "B", + "3d": "", + "3d_link": "", + "issue": "", + "colorcode": "2", + "statusphrase": "Ausleihbar, bitte bestellen", + "link": "", + "status": "NM", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "parts": {} +} diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index 5d120fdfc..f516d70c2 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -90,4 +90,5 @@ liegt seit %s zur Benutzung in Bibliothek bereit Magazinbestellung seit %s in Arbeit vorgemerkt, Pos. %s + Exemplar diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index 42363136f..d8f7960d8 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -91,4 +91,5 @@ ready since %s for usage in library stack request in progress since %s reserved, pos. %s + Copy From d8e86bf0f7d8e115085b8ece43066cee2b9c51f8 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 29 Jan 2020 22:42:12 +0100 Subject: [PATCH 18/55] Fix code analysis issues and simplify isRenewable assignment --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 26 ++++++++----------- .../geeksfactory/opacclient/apis/SLUBTest.kt | 10 +++---- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index a3b9dc9a5..742d86fa3 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2020 by Steffen Rehberg under the MIT license: * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -48,7 +48,7 @@ import org.jsoup.parser.Parser */ open class SLUB : OkHttpBaseApi() { protected lateinit var baseurl: String - protected val ENCODING = "UTF-8" + private val ENCODING = "UTF-8" protected lateinit var query: List private val mediaTypes = mapOf( @@ -200,13 +200,11 @@ open class SLUB : OkHttpBaseApi() { this.id = id val record = json.optJSONObject("record") for (key in record.keys()) { - val v = record.get(key as String) - var value = when (v) { + var value = when (val v = record.get(key as String)) { is String -> v is Int -> v.toString() is JSONArray -> 0.until(v.length()).map { - val arrayItem = v.get(it) - when (arrayItem) { + when (val arrayItem = v.get(it)) { is String -> arrayItem is JSONObject -> arrayItem.optString("title").also { // if item is part of multiple collections, collectionsId holds the last one @@ -219,7 +217,7 @@ open class SLUB : OkHttpBaseApi() { } if (value.isNotEmpty()) { value = Parser.unescapeEntities(value, false) - if (key.equals("title")) { + if (key == "title") { title = value } addDetail(Detail(fieldCaptions[key], value)) @@ -231,7 +229,7 @@ open class SLUB : OkHttpBaseApi() { } } // links and references - for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ + for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ val linkArray = json.optJSONArray(link) linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map{ // assuming that only on of material, note or hostlabel is set @@ -248,7 +246,7 @@ open class SLUB : OkHttpBaseApi() { // copies val cps = json.opt("copies") if (cps is JSONArray) { - getCopies(cps, dateFormat)?.let { copies = it } + getCopies(cps, dateFormat).let { copies = it } } else { // multiple arrays val copiesList = mutableListOf() for (key in (cps as JSONObject).keys()) { @@ -262,7 +260,7 @@ open class SLUB : OkHttpBaseApi() { isReservable = hasReservableCopies // volumes volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { - 0.until(length()).map { optJSONObject(it) }?.map { + 0.until(length()).map { optJSONObject(it) }.map { Volume(it.optString("id"), "${it.optString("part")} ${Parser.unescapeEntities(it.optString("name"),false)}") } @@ -397,7 +395,7 @@ open class SLUB : OkHttpBaseApi() { internal fun parseAccountData(account: Account, json: JSONObject): AccountData { val fmt = DateTimeFormat.shortDate() fun getReservations(items: JSONObject?): MutableList { - val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") + val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" val reservationsList = mutableListOf() for (type in types) { @@ -470,11 +468,9 @@ open class SLUB : OkHttpBaseApi() { it.optInt("X_is_reserved") != 0 -> "vorgemerkt" else -> null } - isRenewable = if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items + if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items + isRenewable = true prolongData = barcode - true - } else { - false } } } ?: emptyList() diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 3313f20af..ebd737a07 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -85,8 +85,8 @@ private class TestStringProvider : StringProvider { } } -class SLUBAccountTest() : BaseHtmlTest() { - var slub = SLUB() +class SLUBAccountTest : BaseHtmlTest() { + private var slub = SLUB() init { slub.stringProvider = TestStringProvider() @@ -174,8 +174,8 @@ class SLUBAccountTest() : BaseHtmlTest() { } } -class SLUBSearchTest() : BaseHtmlTest() { - var slub = SLUB() +class SLUBSearchTest : BaseHtmlTest() { + private var slub = SLUB() @Test fun testParseEmptySearchResults() { @@ -279,7 +279,7 @@ class SLUBSearchTest() : BaseHtmlTest() { @Test fun testParseResultByIdMultipleParts() { val json = JSONObject(readResource("/slub/search/item-multiple_parts_item.json")) - val volumes = listOf( + val volumes = listOf( Volume("0-1453040935", "[3]: Principles of digital image processing"), Volume("0-1347927328", "[2]: Principles of digital image processing"), Volume("0-1347930884", "[1]: Principles of digital image processing") From 941928858e022fcc7c33c8297f7c65bf1b023302 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Fri, 31 Jan 2020 15:37:40 +0100 Subject: [PATCH 19/55] Refactor mapping of json array to map for pickup points to simplify code --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 742d86fa3..73bc5eadd 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -322,10 +322,10 @@ open class SLUB : OkHttpBaseApi() { try { val json = requestAccount(account, "pickup", mapOf("tx_slubaccount_account[barcode]" to data[1])) pickupLocations = json.optJSONArray("PickupPoints")?.run { - 0.until(length()).map { optString(it) }.map { - it to pickupPoints.getOrElse(it){it} + 0.until(length()).map { optString(it) }.associateWith { + pickupPoints.getOrElse(it) { it } } - }?.toMap() ?: emptyMap() + } ?: emptyMap() } catch (e: OpacApi.OpacErrorException) { return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) } From ce9a70664c5b1020ce6bbc23632ee7758d2aec49 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 5 Jan 2019 18:34:55 +0100 Subject: [PATCH 20/55] Add SLUB API, implement parseSearchFields and getSupportFlags This API is specific for SLUB in Dresden. --- .../opacclient/OpacApiFactory.java | 3 + .../de/geeksfactory/opacclient/apis/SLUB.kt | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java index 02deac1ef..d45d6c11d 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/OpacApiFactory.java @@ -35,6 +35,7 @@ import de.geeksfactory.opacclient.apis.Primo; import de.geeksfactory.opacclient.apis.SISIS; import de.geeksfactory.opacclient.apis.SLIM; +import de.geeksfactory.opacclient.apis.SLUB; import de.geeksfactory.opacclient.apis.SRU; import de.geeksfactory.opacclient.apis.TestApi; import de.geeksfactory.opacclient.apis.TouchPoint; @@ -141,6 +142,8 @@ public static OpacApi create(Library lib, StringProvider sp, HttpClientFactory h newApiInstance = new Koha(); } else if (lib.getApi().equals("netbiblio")) { newApiInstance = new NetBiblio(); + } else if (lib.getApi().equals("slub")) { + newApiInstance = new SLUB(); } else if (lib.getApi().equals("arena")) { newApiInstance = new Arena(); } else if (lib.getApi().equals("slim")) { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt new file mode 100644 index 000000000..5388a66a8 --- /dev/null +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -0,0 +1,98 @@ +package de.geeksfactory.opacclient.apis + +import de.geeksfactory.opacclient.networking.HttpClientFactory +import de.geeksfactory.opacclient.objects.* +import de.geeksfactory.opacclient.searchfields.SearchField +import de.geeksfactory.opacclient.searchfields.SearchQuery +import de.geeksfactory.opacclient.searchfields.TextSearchField +import de.geeksfactory.opacclient.utils.get +import de.geeksfactory.opacclient.utils.html +import de.geeksfactory.opacclient.utils.text + +/** + * OpacApi implementation for SLUB. https://slub-dresden.de + * + * @author Steffen Rehberg, Jan 2019 + */ +open class SLUB : OkHttpBaseApi() { + protected lateinit var baseurl: String + protected val ENCODING = "UTF-8" + + override fun init(library: Library, factory: HttpClientFactory) { + super.init(library, factory) + baseurl = library.data.getString("baseurl") + } + + override fun search(query: List): SearchRequestResult { + TODO("not implemented") + } + + override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { + TODO("not implemented") + } + + override fun searchGetPage(page: Int): SearchRequestResult { + TODO("not implemented") + } + + override fun getResultById(id: String, homebranch: String?): DetailedItem { + TODO("not implemented") + } + + override fun getResult(position: Int): DetailedItem? { + // getResultById is implemented and every search result has an id set, so getResult is not used + return null + } + + override fun reservation(item: DetailedItem, account: Account, useraction: Int, selection: String?): OpacApi.ReservationResult { + TODO("not implemented") + } + + override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { + TODO("not implemented") + } + + override fun prolongAll(account: Account, useraction: Int, selection: String?): OpacApi.ProlongAllResult { + TODO("not implemented") + } + + override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { + TODO("not implemented") + } + + override fun account(account: Account): AccountData { + TODO("not implemented") + } + + override fun checkAccountData(account: Account) { + TODO("not implemented") + } + + override fun getShareUrl(id: String?, title: String?): String { + TODO("not implemented") + } + + override fun getSupportFlags(): Int { + return 0 + } + + override fun getSupportedLanguages(): Set? { + //TODO("not implemented") + return null + } + + override fun parseSearchFields(): List { + val doc = httpGet(baseurl, ENCODING).html + return doc.select("ul#search-in-field-options li").map { + TextSearchField().apply { + id = it["name"] + displayName = it.text + } + } + } + + override fun setLanguage(language: String?) { + //TODO("not implemented") + } + +} \ No newline at end of file From 36ab2ce85e4d6465381f914a4adc8c6e9c78d4b9 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 2 Feb 2019 23:50:20 +0100 Subject: [PATCH 21/55] Implement search, searchGetPage, getResultById, prolong, account, checkAccountData --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 227 +++++++++++++++++- 1 file changed, 216 insertions(+), 11 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 5388a66a8..dca87667b 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -1,5 +1,6 @@ package de.geeksfactory.opacclient.apis +import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* import de.geeksfactory.opacclient.searchfields.SearchField @@ -8,6 +9,15 @@ import de.geeksfactory.opacclient.searchfields.TextSearchField import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text +import okhttp3.FormBody +import org.joda.time.LocalDateTime +import org.joda.time.format.DateTimeFormat +import org.joda.time.format.DateTimeFormatter +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.jsoup.Jsoup +import org.jsoup.parser.Parser /** * OpacApi implementation for SLUB. https://slub-dresden.de @@ -17,6 +27,35 @@ import de.geeksfactory.opacclient.utils.text open class SLUB : OkHttpBaseApi() { protected lateinit var baseurl: String protected val ENCODING = "UTF-8" + protected lateinit var query: List + + private val mediaTypes = mapOf( + "Article, E-Article" to SearchResult.MediaType.EDOC, + "Book, E-Book" to SearchResult.MediaType.BOOK, + "Video" to SearchResult.MediaType.EVIDEO, + "Thesis" to SearchResult.MediaType.BOOK, + "Manuscript" to SearchResult.MediaType.BOOK, + "Musical Score" to SearchResult.MediaType.SCORE_MUSIC, + "Website" to SearchResult.MediaType.URL, + "Journal, E-Journal" to SearchResult.MediaType.NEWSPAPER, + "Map" to SearchResult.MediaType.MAP, + "Audio" to SearchResult.MediaType.EAUDIO, + "Image" to SearchResult.MediaType.ART, + //"Microfrom" to SearchResult.MediaType.MICROFORM //TODO: define new media type microform + "Visual Media" to SearchResult.MediaType.ART + ) + + private val fieldCaptions = mapOf( + "format" to "Medientyp", + "title" to "Titel", + "contributor" to "Beteiligte", + "publisher" to "Erschienen", + "ispartof" to "Erschienen in", + "identifier" to "ISBN", + "language" to "Sprache", + "subject" to "Schlagwörter", + "description" to "Beschreibung" + ) override fun init(library: Library, factory: HttpClientFactory) { super.init(library, factory) @@ -24,23 +63,121 @@ open class SLUB : OkHttpBaseApi() { } override fun search(query: List): SearchRequestResult { - TODO("not implemented") + this.query = query + return searchGetPage(1) } - override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { - TODO("not implemented") + override fun searchGetPage(page: Int): SearchRequestResult { + val queryfbb = FormBody.Builder() + .add("type", "1369315142") + .add("tx_find_find[format]", "data") + .add("tx_find_find[data-format]", "app") + .add("tx_find_find[page]", page.toString()) + for (sq in query) { + if (sq.value.isNotEmpty()) { + queryfbb.add("tx_find_find[q][${sq.key}]", sq.value) + } + } + val queryfb = queryfbb.build() + if (queryfb.size() <= 4) + throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) + val json = JSONObject(httpPost(baseurl, queryfb, ENCODING)) + return SearchRequestResult(json.optJSONArray("docs") + ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } + ?.map { + SearchResult().apply { + innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) + ?: ""}
(${it.optString("creationDate")})" + type = mediaTypes[it.optJSONArray("format")?.optString(0)] + id = it.optString("id") + } + }, json.optInt("numFound"), 1) + //TODO: get status (one request per item!) } - override fun searchGetPage(page: Int): SearchRequestResult { + override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { TODO("not implemented") } override fun getResultById(id: String, homebranch: String?): DetailedItem { - TODO("not implemented") + + fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = + copiesArray.run { 0.until(length()).map { optJSONObject(it) } } + .map { + Copy().apply { + barcode = it.getString("barcode") + branch = it.getString("location") + department = it.getString("sublocation") // or location = ... + shelfmark = it.getString("shelfmark") + status = Jsoup.parse(it.getString("statusphrase")).text() + it.getString("duedate").run { + if (isNotEmpty()) { + returnDate = df.parseLocalDate(this) + } + } + if (it.getString("vormerken") == "1") { + resInfo = barcode + } + // reservations: only available for reserved copies, not for reservable copies + // url: not for accessible online resources, only for lendable online copies + } + } + + val detailfb = FormBody.Builder() + .add("type", "1369315142") + .add("tx_find_find[format]", "data") + .add("tx_find_find[data-format]", "app") + val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) + val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") + + return DetailedItem().apply { + this.id = id + val record = json.optJSONObject("record") + for (key in record.keys()) { + val v = record.get(key as String) + var value = when (v) { + is String -> v + is Int -> v.toString() + is JSONArray -> 0.until(v.length()).map { + val arrayItem = v.get(it) + when (arrayItem) { + is String -> arrayItem + is JSONObject -> arrayItem.optString("title") + else -> null + } + }.joinToString("; ") + else -> "" + } + if (value.isNotEmpty()) { + value = Parser.unescapeEntities(value, false) + if (key.equals("title")) { + title = value + } + addDetail(Detail(fieldCaptions[key], value)) + } + } + val cps = json.opt("copies") + if (cps is JSONArray) { + getCopies(cps, dateFormat)?.let { copies = it } + } else { // multiple arrays + val copiesList = mutableListOf() + for (key in (cps as JSONObject).keys()) { + val cpsi = cps.get(key as String) + if (cpsi is JSONArray) { + copiesList.addAll(getCopies(cpsi, dateFormat)) + } + } + copies = copiesList + } + // TODO: volumes + // TODO: collectionid + // TODO: add linksAccess as detail (uri & hostLabel, note?, material?) + // TODO: add other links (links, linksRelated, linksGeneral) as details? + } } override fun getResult(position: Int): DetailedItem? { - // getResultById is implemented and every search result has an id set, so getResult is not used + // not used (getResultById is implemented and every search result has an id set) return null } @@ -49,11 +186,16 @@ open class SLUB : OkHttpBaseApi() { } override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { - TODO("not implemented") + return try { + requestAccount(account, "renew", mapOf("tx_slubaccount_account[renewals][0]" to media)) + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK) + } catch (e: Exception) { + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } } override fun prolongAll(account: Account, useraction: Int, selection: String?): OpacApi.ProlongAllResult { - TODO("not implemented") + return OpacApi.ProlongAllResult(OpacApi.MultiStepResult.Status.UNSUPPORTED) } override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { @@ -61,11 +203,74 @@ open class SLUB : OkHttpBaseApi() { } override fun account(account: Account): AccountData { - TODO("not implemented") + val fmt = DateTimeFormat.shortDate() + val json = requestAccount(account, "account") + return AccountData(account.id).apply { + pendingFees = json.optJSONObject("fees")?.optString("topay_list") + validUntil = json.optJSONObject("memberInfo")?.optString("expires") + ?.substring(0, 10)?.let { fmt.print(LocalDateTime(it)) } + lent = json.optJSONObject("items")?.optJSONArray("loan") + ?.run { 0.until(length()).map { optJSONObject(it) } } + ?.map { + LentItem().apply { + title = it.optString("about") + author = it.optJSONArray("X_author")?.optString(0) + setDeadline(it.optString("X_date_due")) + format = it.optString("X_medientyp") + barcode = it.optString("X_barcode") + status = when { + it.optInt("renewals") == 2 -> "2x verlängert" + it.optInt("X_is_reserved") != 0 -> "vorgemerkt" + else -> null + } + isRenewable = if (it.optInt("X_is_renewable") == 1) { + prolongData = barcode + true + } else { + false + } + } + } ?: emptyList() + reservations = json.optJSONObject("items")?.optJSONArray("reserve") + ?.run { 0.until(length()).toMutableList().map { optJSONObject(it) } } + ?.map { + ReservedItem().apply { + title = it.optString("about") + author = it.optJSONArray("X_author")?.optString(0) + format = it.optString("X_medientyp") + status = it.optInt("X_queue_number").let { "Pos. $it" } + } + } ?: emptyList() + } + } + + private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { + val formBody = FormBody.Builder() + .add("type", "1") + .add("tx_slubaccount_account[controller]", "API") + .add("tx_slubaccount_account[action]", action) + .add("tx_slubaccount_account[username]", account.name) + .add("tx_slubaccount_account[password]", account.password) + parameters?.map { + formBody.add(it.key, it.value) + } + try { + return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { + if (it.optInt("status") != 1) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, + it.optString("message", "error requesting account data"))) + } + } + } catch (e: JSONException) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, + "accountRequest didn't return JSON object: ${e.message}")) + } } override fun checkAccountData(account: Account) { - TODO("not implemented") + requestAccount(account, "validate") } override fun getShareUrl(id: String?, title: String?): String { @@ -95,4 +300,4 @@ open class SLUB : OkHttpBaseApi() { //TODO("not implemented") } -} \ No newline at end of file +} From 40f224735c02638b8cdf6428d1c52f06740c8b79 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 3 Feb 2019 13:03:51 +0100 Subject: [PATCH 22/55] Implement getShareUrl --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index dca87667b..28b2c414a 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -274,7 +274,7 @@ open class SLUB : OkHttpBaseApi() { } override fun getShareUrl(id: String?, title: String?): String { - TODO("not implemented") + return "$baseurl/id/$id" } override fun getSupportFlags(): Int { From ac04f22f6ed05b6cea26cfbcda86e84691bebbcb Mon Sep 17 00:00:00 2001 From: StefRe Date: Tue, 5 Feb 2019 20:51:28 +0100 Subject: [PATCH 23/55] Add two more mediatypes --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 28b2c414a..cb3ab5519 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -40,8 +40,9 @@ open class SLUB : OkHttpBaseApi() { "Journal, E-Journal" to SearchResult.MediaType.NEWSPAPER, "Map" to SearchResult.MediaType.MAP, "Audio" to SearchResult.MediaType.EAUDIO, + "Electronic Resource (Data Carrier)" to SearchResult.MediaType.EAUDIO, "Image" to SearchResult.MediaType.ART, - //"Microfrom" to SearchResult.MediaType.MICROFORM //TODO: define new media type microform + "Microform" to SearchResult.MediaType.MICROFORM, "Visual Media" to SearchResult.MediaType.ART ) From b0de98163932caa45fd031684279c73c86dc3af0 Mon Sep 17 00:00:00 2001 From: StefRe Date: Mon, 30 Dec 2019 08:02:05 +0100 Subject: [PATCH 24/55] Refactor for testing, add tests for account, search result and detailed item --- opacclient/libopac/build.gradle | 1 + .../de/geeksfactory/opacclient/apis/SLUB.kt | 67 ++- .../geeksfactory/opacclient/apis/SLUBTest.kt | 132 ++++++ .../test/resources/slub/account/account.json | 190 +++++++++ .../resources/slub/account/empty-account.json | 58 +++ .../resources/slub/search/empty-search.json | 47 ++ .../item-copies_in_multiple_arrays.json | 400 ++++++++++++++++++ .../resources/slub/search/simple-item.json | 78 ++++ .../resources/slub/search/simple-search.json | 131 ++++++ 9 files changed, 1081 insertions(+), 23 deletions(-) create mode 100644 opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt create mode 100644 opacclient/libopac/src/test/resources/slub/account/account.json create mode 100644 opacclient/libopac/src/test/resources/slub/account/empty-account.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/empty-search.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/simple-item.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/simple-search.json diff --git a/opacclient/libopac/build.gradle b/opacclient/libopac/build.gradle index edba554e8..0e957aac4 100644 --- a/opacclient/libopac/build.gradle +++ b/opacclient/libopac/build.gradle @@ -29,6 +29,7 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.hamcrest:hamcrest-library:2.2' testImplementation 'org.mockito:mockito-core:1.10.19' + testImplementation 'com.shazam:shazamcrest:0.11' } task copyTestResources(type: Copy) { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index cb3ab5519..ecd791d31 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -83,8 +83,11 @@ open class SLUB : OkHttpBaseApi() { if (queryfb.size() <= 4) throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) val json = JSONObject(httpPost(baseurl, queryfb, ENCODING)) - return SearchRequestResult(json.optJSONArray("docs") - ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } + return parseSearchResults(json) + } + + internal fun parseSearchResults(json: JSONObject): SearchRequestResult{ + val searchresults = json.optJSONArray("docs")?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } ?.map { SearchResult().apply { innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) @@ -92,8 +95,9 @@ open class SLUB : OkHttpBaseApi() { type = mediaTypes[it.optJSONArray("format")?.optString(0)] id = it.optString("id") } - }, json.optInt("numFound"), 1) + } //TODO: get status (one request per item!) + return SearchRequestResult(searchresults, json.optInt("numFound"), 1) } override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { @@ -101,7 +105,16 @@ open class SLUB : OkHttpBaseApi() { } override fun getResultById(id: String, homebranch: String?): DetailedItem { + val detailfb = FormBody.Builder() + .add("type", "1369315142") + .add("tx_find_find[format]", "data") + .add("tx_find_find[data-format]", "app") + val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) + return parseResultById(id, json) + } + internal fun parseResultById(id:String, json: JSONObject): DetailedItem { + val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = copiesArray.run { 0.until(length()).map { optJSONObject(it) } } .map { @@ -123,14 +136,6 @@ open class SLUB : OkHttpBaseApi() { // url: not for accessible online resources, only for lendable online copies } } - - val detailfb = FormBody.Builder() - .add("type", "1369315142") - .add("tx_find_find[format]", "data") - .add("tx_find_find[data-format]", "app") - val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) - val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") - return DetailedItem().apply { this.id = id val record = json.optJSONObject("record") @@ -204,13 +209,38 @@ open class SLUB : OkHttpBaseApi() { } override fun account(account: Account): AccountData { - val fmt = DateTimeFormat.shortDate() val json = requestAccount(account, "account") + return parseAccountData(account, json) + } + + internal fun parseAccountData(account: Account, json: JSONObject): AccountData { + fun getReservations(items: JSONObject?): MutableList { + val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve", "ill") + // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" + val reservationsList = mutableListOf() + for (type in types) { + items?.optJSONArray(type)?.let { + for (i in 0 until it.length()) { + reservationsList.add(it.getJSONObject(i).let { + ReservedItem().apply { + title = it.optString("about") + author = it.optJSONArray("X_author")?.optString(0) + format = it.optString("X_medientyp") + status = it.optInt("X_queue_number").let { "Pos. $it" } + } + }) + } + } + } + return reservationsList + } + + val fmt = DateTimeFormat.shortDate() return AccountData(account.id).apply { pendingFees = json.optJSONObject("fees")?.optString("topay_list") validUntil = json.optJSONObject("memberInfo")?.optString("expires") ?.substring(0, 10)?.let { fmt.print(LocalDateTime(it)) } - lent = json.optJSONObject("items")?.optJSONArray("loan") + lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } ?.map { LentItem().apply { @@ -232,16 +262,7 @@ open class SLUB : OkHttpBaseApi() { } } } ?: emptyList() - reservations = json.optJSONObject("items")?.optJSONArray("reserve") - ?.run { 0.until(length()).toMutableList().map { optJSONObject(it) } } - ?.map { - ReservedItem().apply { - title = it.optString("about") - author = it.optJSONArray("X_author")?.optString(0) - format = it.optString("X_medientyp") - status = it.optInt("X_queue_number").let { "Pos. $it" } - } - } ?: emptyList() + reservations = getReservations(json.optJSONObject("items")) } } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt new file mode 100644 index 000000000..a3b354686 --- /dev/null +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -0,0 +1,132 @@ +package de.geeksfactory.opacclient.apis + +import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs +import de.geeksfactory.opacclient.objects.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + + +class SLUBAccountTest() : BaseHtmlTest() { + var slub = SLUB() + + @Test + fun testParseEmptyAccountData() { + val json = JSONObject(readResource("/slub/account/empty-account.json")) + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals("1,23 EUR", accountdata.pendingFees) + assertEquals("31.03.20", accountdata.validUntil) + assertTrue(accountdata.lent.isEmpty()) + assertTrue(accountdata.reservations.isEmpty()) + } + + @Test + fun testParseAccountData() { + val json = JSONObject(readResource("/slub/account/account.json")) + val lentitem1 = LentItem().apply { + title = "¬Der¬ neue Kosmos-Baumführer" + author = "Bachofer, Mark" + setDeadline("2019-06-03") + format = "B" + barcode = "31626878" + isRenewable = true + prolongData = barcode + } + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals(2, accountdata.lent.size) + assertEquals(3, accountdata.reservations.size) + assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) + assertEquals("vorgemerkt", accountdata.lent[1].status) + } +} + +class SLUBSearchTest() : BaseHtmlTest() { + var slub = SLUB() + + @Test + fun testParseEmptySearchResults() { + val json = JSONObject(readResource("/slub/search/empty-search.json")) + + val searchresults = slub.parseSearchResults(json) + + assertEquals(0, searchresults.total_result_count) + assertTrue(searchresults.results.isEmpty()) + } + + @Test + fun testParseSearchResults() { + val json = JSONObject(readResource("/slub/search/simple-search.json")) + val result1 = SearchResult().apply { + innerhtml = "Mastering software testing with JUnit 5 comprehensive guide to develop high quality Java applications Boni García
Garcia, Boni
(2017)" + type = SearchResult.MediaType.BOOK + id = "0-1014939550" + } + + val searchresults = slub.parseSearchResults(json) + + assertEquals(2, searchresults.total_result_count) + assertThat(result1, samePropertyValuesAs(searchresults.results[0])) + } + + @Test + fun testParseResultById() { + val json = JSONObject(readResource("/slub/search/simple-item.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Buch")) + addDetail(Detail("Titel", "Unit-Tests mit JUnit")) + title = "Unit-Tests mit JUnit" + addDetail(Detail("Beteiligte", "Hunt, Andrew; Thomas, David [Autor/In]")) + addDetail(Detail("Erschienen", "München Wien Hanser 2004 ")) + addDetail(Detail("Erschienen in", "Pragmatisch Programmieren; 2")) + addDetail(Detail("ISBN", "3446228241; 3446404694; 9783446404694; 9783446228245")) + addDetail(Detail("Sprache", "Deutsch")) + addDetail(Detail("Schlagwörter", "Quellcode; Softwaretest; JUnit")) + id = "0-1182402208" + copies = arrayListOf(Copy().apply { + barcode = "31541466" + department = "Freihand" + branch = "Bereichsbibliothek DrePunct" + status = "Ausleihbar" + shelfmark = "ST 233 H939" + }) + } + + val item = slub.parseResultById(json.getString("id"), json) + + //details are in unspecified order, see https://stackoverflow.com/a/4920304/3944322 + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(item.details), sameBeanAs(HashSet(expected.details))) + } + + @Test + fun testParseResultByIdCopiesInMultipleArrays() { + val json = JSONObject(readResource("/slub/search/item-copies_in_multiple_arrays.json")) + val copyFirst = Copy().apply { + barcode = "10418078" + department = "Magazin Zeitschriften" + branch = "Zentralbibliothek" + status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" + shelfmark = "19 4 01339 0 0024 1 01" + } + val copyLast = Copy().apply { + barcode = "33364639" + department = "Magazin Zeitschriften" + branch = "Zentralbibliothek" + status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" + shelfmark = "19 4 01339 1 1969 1 01" + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertEquals(19, item.copies.size) + // the copies arrays may occur in any order + assertThat(item.copies, hasItems(sameBeanAs(copyFirst), sameBeanAs(copyLast))) + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/account/account.json b/opacclient/libopac/src/test/resources/slub/account/account.json new file mode 100644 index 000000000..f1286a15c --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/account.json @@ -0,0 +1,190 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2099-01-01T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65143", + "X_date_of_last_issue_ext": "2019-05-10", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "items": { + "loan": [ + { + "status": 3, + "about": "¬Der¬ neue Kosmos-Baumführer", + "label": "31626878", + "queue": "0", + "renewals": 0, + "starttime": "2019-05-05T15:45:39Z", + "endtime": "2019-06-02T22:00:00Z", + "X_status_desc": "on loan", + "X_barcode": "31626878", + "X_author": [ + "Bachofer, Mark", + "Mayer, Joachim" + ], + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_shelfLocation": "WL 9825 B124", + "X_internal_date_issued": "65138,63939", + "X_date_issued": "2019-05-05 17:45:39", + "X_internal_date_due": "65167", + "X_date_due": "2019-06-03", + "X_days_to_due": "20", + "X_is_reserved": 0, + "X_is_renewable": 1, + "X_is_flrenewable": 0, + "X_rsn": 13840872 + }, + { + "status": 3, + "about": "Bäume bestimmen", + "label": "33121334", + "queue": "0", + "renewals": 0, + "starttime": "2019-04-30T10:37:11Z", + "endtime": "2019-05-27T22:00:00Z", + "X_status_desc": "on loan", + "X_barcode": "33121334", + "X_author": [ + "Lüder, Rita" + ], + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_shelfLocation": "WL 9825 L948", + "X_internal_date_issued": "65133,45431", + "X_date_issued": "2019-04-30 12:37:11", + "X_internal_date_due": "65161", + "X_date_due": "2019-05-28", + "X_days_to_due": "14", + "X_is_reserved": 1, + "X_is_renewable": 1, + "X_is_flrenewable": 0, + "X_rsn": 15864711 + } + ], + "reserve": [ + { + "status": 1, + "about": "Pareys Buch der Bäume", + "label": "30963742", + "starttime": "2019-05-10T07:11:09Z", + "X_queue_number": 1, + "X_delete_number": 1, + "X_author": [ + "Mitchell, Alan", + "Wilkinson, John", + "Schütt, Peter ¬[Übers.]¬" + ], + "X_status_desc": "reserved", + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_internal_date_reserved": "65143,33069", + "X_date_reserved": "2019-05-10 09:11:09", + "X_provided": 0, + "X_rsn": 963145 + } + ], + "hold": [ + { + "status": 4, + "about": "Welcher Baum ist das?", + "label": "34778398", + "starttime": "2019-05-09T22:00:00Z", + "X_status_desc": "on hold", + "X_pickup_code": "tha1", + "X_pickup_desc": "ZwB Forstwissenschaft", + "X_medientyp": "B", + "X_exstatus": "N", + "X_exstatus_desc": "Ausleihbar ", + "X_ill_duedate": "", + "X_ill_request_id": "", + "X_author": [ + "Mayer, Joachim ¬[VerfasserIn]¬", + "Schwegler, Heinz Werner ¬[VerfasserIn]¬" + ], + "X_internal_date_reserved": "65143", + "X_date_reserved": "2019-05-10", + "X_provided": 0, + "X_rsn": 16605483 + } + ], + "request_ready": [ + { + "status": 1, + "about": "Englische Synonyme als Fehlerquellen", + "label": "20550495", + "starttime": "2019-05-04T06:35:39Z", + "X_status_desc": "requested", + "X_author": [ + "Meyer, Jürgen", + "Schulz, Gisela" + ], + "X_stackid": "2038188", + "X_systemmessage": "http://libero6.slub-dresden.de:57772/csp/user/paia/coremsg.csp?patronid=4222666&msgid=2038188", + "X_medientyp": "B", + "X_exstatus": "NM", + "X_exstatus_desc": "Ausleihbar ", + "X_internal_date_requested": "65137,30939", + "X_date_requested": "2019-05-04 08:35:39", + "X_pickup_code": "a01", + "X_pickup_desc": "Zentralbibliothek Ebene 0 SB-Regal", + "X_stacklocation": "m002", + "X_request": "l", + "X_request_desc": "liegt bereit", + "X_rsn": 1494121 + } + ] + }, + "fees": { + "amount": "0,00 EUR", + "subset": "open", + "amount_list": "0,00 EUR", + "paid_all": "0,00 EUR", + "paid_list": "0,00 EUR", + "fee_all": "0,00 EUR", + "fee_list": "0,00 EUR", + "topay_all": "0,00 EUR", + "topay_list": "0,00 EUR", + "overdue_paid": "0,00 EUR", + "overdue_fee": "0,00 EUR", + "ill_paid": "0,00 EUR", + "ill_fee": "0,00 EUR", + "other_paid": "0,00 EUR", + "other_fee": "0,00 EUR", + "fee": [] + }, + "arguments": { + "controller": "API", + "action": "account", + "username": "1234567" + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/account/empty-account.json b/opacclient/libopac/src/test/resources/slub/account/empty-account.json new file mode 100644 index 000000000..4f52b21f6 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/empty-account.json @@ -0,0 +1,58 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2020-03-31T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65143", + "X_date_of_last_issue_ext": "2019-05-10", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "fees": { + "amount": "0,00 EUR", + "subset": "open", + "amount_list": "0,00 EUR", + "paid_all": "0,00 EUR", + "paid_list": "0,00 EUR", + "fee_all": "0,00 EUR", + "fee_list": "0,00 EUR", + "topay_all": "0,00 EUR", + "topay_list": "1,23 EUR", + "overdue_paid": "0,00 EUR", + "overdue_fee": "0,00 EUR", + "ill_paid": "0,00 EUR", + "ill_fee": "0,00 EUR", + "other_paid": "0,00 EUR", + "other_fee": "0,00 EUR", + "fee": [] + }, + "arguments": { + "controller": "API", + "action": "account", + "username": "1234567" + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/empty-search.json b/opacclient/libopac/src/test/resources/slub/search/empty-search.json new file mode 100644 index 000000000..bbee79a08 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/empty-search.json @@ -0,0 +1,47 @@ +{ + "numFound": 0, + "start": 0, + "docs": [], + "facets": { + "access_facet": { + "translation": "Zugang", + "values": [] + }, + "format_de15": { + "translation": "Medientyp", + "values": [] + }, + "publishDateSort": { + "translation": "Erscheinungsjahr", + "values": [] + }, + "branch": { + "translation": "Standort", + "values": [] + }, + "language": { + "translation": "Sprache", + "values": [] + }, + "thema": { + "translation": "Fachgebiet", + "values": [] + }, + "author": { + "translation": "Person/Institution", + "values": [] + }, + "facet_music_notation_de14": { + "translation": "musikalische Ausgabeform", + "values": [] + }, + "music_heading_browse": { + "translation": "musikalische Besetzung", + "values": [] + }, + "mega_collection": { + "translation": "Kollektion", + "values": [] + } + } +} diff --git a/opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json b/opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json new file mode 100644 index 000000000..534d88f29 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-copies_in_multiple_arrays.json @@ -0,0 +1,400 @@ +{ + "record": { + "format": "Zeitschrift", + "title": "Thyssen Technische Berichte", + "contributor": [ + "Thyssen-Aktiengesellschaft, Vormals August-Thyssen-Hütte", + "Thyssen-Gruppe", + "Thyssen-Stahl-Aktiengesellschaft" + ], + "publisher": [ + "Duisburg Thyssen 1974-1993 " + ], + "ispartof": [], + "identifier": "", + "language": [ + "Deutsch" + ], + "subject": [ + "Stahl", + "Zeitschrift", + "Werkstoffkunde", + "Zeitschrift", + "Eisen- und Stahlindustrie", + "Zeitschrift" + ], + "description": [ + "Beteil. Körp. 6.1974 - 8.1976: August-Thyssen-Hütte, Abteilung Zentrale Forschung der Thyssen-Gruppe; 9.1977 - 21.1989: Thyssen-Aktiengesellschaft, vormals August-Thyssen-Hütte." + ], + "status": "", + "rvk": "" + }, + "id": "0-130446319", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [ + { + "text": "Vorgänger", + "link": "http://slubdd.de/katalog?libero_mab21364124", + "name": "August-Thyssen-Hütte: Thyssen-Forschung", + "target": "SLUB" + } + ], + "copies": { + "1990 - 1999": [ + { + "barcode": "10418078", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0024 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 24.1992", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10076153", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0023 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 23.1991", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10076694", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0022 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 22.1990", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "1980 - 1989": [ + { + "barcode": "10075519", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0021 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 21.1989", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10076756", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0020 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 20.1988", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10077027", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0019 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 19.1987", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "10075809", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0018 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 18.1986", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364662", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0017 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 17.1985", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364651", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0016 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 16.1984", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364640", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0015 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 15.1983", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364663", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0014 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 14.1982", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364652", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0013 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 13.1981", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364641", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0012 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 12.1980,1", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "1970 - 1979": [ + { + "barcode": "33364140", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0011 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 11.1979", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364141", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0010 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 10.1978", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364152", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0009 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 9.1977", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364153", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0008 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 8.1976,1", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + }, + { + "barcode": "33364142", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 0 0007 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 7.1975,1", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "1960 - 1969": [ + { + "barcode": "33364639", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin Zeitschriften", + "shelfmark": "19 4 01339 1 1969 1 01", + "mediatype": "ZS", + "3d": "", + "3d_link": "", + "issue": " 1969/78,Index", + "colorcode": "2", + "statusphrase": "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung", + "link": "", + "status": "PM2", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ] + }, + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/simple-item.json b/opacclient/libopac/src/test/resources/slub/search/simple-item.json new file mode 100644 index 000000000..ddee76619 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/simple-item.json @@ -0,0 +1,78 @@ +{ + "record": { + "format": "Buch", + "title": "Unit-Tests mit JUnit", + "contributor": [ + "Hunt, Andrew", + "Thomas, David [Autor/In]" + ], + "publisher": [ + "München Wien Hanser 2004 " + ], + "ispartof": [ + { + "id": "0-1183957874", + "title": "Pragmatisch Programmieren; 2" + } + ], + "identifier": [ + "3446228241", + "3446404694", + "9783446404694", + "9783446228245" + ], + "language": [ + "Deutsch" + ], + "subject": [ + "Quellcode", + "Softwaretest", + "JUnit" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "0-1182402208", + "oa": 0, + "thumbnail": "", + "links": [ + { + "uri": "http://d-nb.info/970689268/04", + "note": "", + "material": "Inhaltsverzeichnis" + } + ], + "linksRelated": [ + { + "uri": "http://d-nb.info/970689268/04", + "hostLabel": "", + "note": "", + "material": "Inhaltsverzeichnis" + } + ], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [ + { + "barcode": "31541466", + "location": "Bereichsbibliothek DrePunct", + "location_code": "zell9", + "sublocation": "Freihand", + "shelfmark": "ST 233 H939", + "mediatype": "B", + "3d": "ST 233 H939", + "3d_link": "https://3d.slub-dresden.de/viewer?project_id=3&search_key=ST%20233%20H939&language=de&search_context1=zell9&search_context2=FH1&exemplar_id=31541466", + "issue": "", + "colorcode": "1", + "statusphrase": "Ausleihbar", + "link": "", + "status": "N", + "duedate": "", + "vormerken": "0", + "bestellen": "0" + } + ], + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/simple-search.json b/opacclient/libopac/src/test/resources/slub/search/simple-search.json new file mode 100644 index 000000000..689fe67f5 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/simple-search.json @@ -0,0 +1,131 @@ +{ + "numFound": 2, + "start": 0, + "docs": [ + { + "id": "0-1014939550", + "format": [ + "Book, E-Book" + ], + "title": "Mastering software testing with JUnit 5 comprehensive guide to develop high quality Java applications Boni García", + "author": [ + "Garcia, Boni" + ], + "creationDate": "2017", + "imprint": [ + "Birmingham, UK: Packt Publishing, 2017" + ] + }, + { + "id": "dswarm-141-NzE4NjM5", + "format": [ + "Video" + ], + "title": "Learning JUnit 5", + "author": [ + "Boni García" + ], + "creationDate": "2018", + "imprint": [ + "Carpenteria : Lynda, 2018" + ] + } + ], + "facets": { + "access_facet": { + "translation": "Zugang", + "values": { + "Electronic Resources": { + "translation": "Online-Ressourcen", + "values": 2 + } + } + }, + "format_de15": { + "translation": "Medientyp", + "values": { + "Book, E-Book": { + "translation": "Bücher", + "values": 1 + }, + "Video": { + "translation": "Video", + "values": 1 + } + } + }, + "publishDateSort": { + "translation": "Erscheinungsjahr", + "values": { + "2017": { + "translation": "2017", + "values": 1 + }, + "2018": { + "translation": "2018", + "values": 1 + } + } + }, + "branch": { + "translation": "Standort", + "values": [] + }, + "language": { + "translation": "Sprache", + "values": { + "English": { + "translation": "Englisch", + "values": 2 + } + } + }, + "thema": { + "translation": "Fachgebiet", + "values": { + "Informatik": { + "translation": "Informatik", + "values": 1 + }, + "Mathematik": { + "translation": "Mathematik", + "values": 1 + } + } + }, + "author": { + "translation": "Person/Institution", + "values": { + "Boni García": { + "translation": "Boni García", + "values": 1 + }, + "Garcia, Boni": { + "translation": "Garcia, Boni", + "values": 1 + } + } + }, + "facet_music_notation_de14": { + "translation": "musikalische Ausgabeform", + "values": [] + }, + "music_heading_browse": { + "translation": "musikalische Besetzung", + "values": [] + }, + "mega_collection": { + "translation": "Kollektion", + "values": { + "Lynda.com Englisch": { + "translation": "Lynda.com Englisch", + "values": 1 + }, + "Verbunddaten SWB": { + "translation": "Verbunddaten SWB", + "values": 1 + } + } + } + } +} From 1711e272ca43686957cfe9aa4eb55fa7b5ca6404 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Fri, 3 Jan 2020 12:45:49 +0100 Subject: [PATCH 25/55] Include year in search results only if not "null" --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 7 +- .../geeksfactory/opacclient/apis/SLUBTest.kt | 15 +++ .../search/search-null_creation_date.json | 111 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index ecd791d31..f3efcd571 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -91,7 +91,12 @@ open class SLUB : OkHttpBaseApi() { ?.map { SearchResult().apply { innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) - ?: ""}
(${it.optString("creationDate")})" + ?: ""}" + it.optString("creationDate")?.run { + if (this != "null") { + innerhtml += "
(${this})" + } + } type = mediaTypes[it.optJSONArray("format")?.optString(0)] id = it.optString("id") } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index a3b354686..ff8ff0e4c 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -75,6 +75,21 @@ class SLUBSearchTest() : BaseHtmlTest() { assertThat(result1, samePropertyValuesAs(searchresults.results[0])) } + @Test + fun testParseSearchResultsWithNullCreationDate() { + val json = JSONObject(readResource("/slub/search/search-null_creation_date.json")) + val result1 = SearchResult().apply { + innerhtml = "Tu en hagiois patros hēmōn Maximu tu homologetu Hapanta = S.P.N. Maximi Confessoris Opera omnia eruta, Latine transl., notisque ill. opera et studio Francisci Combefis. Adauxit Franciscus Oehler. Accurante et denuo recognoscente J.-P. Migne
Maximus Confessor" + type = SearchResult.MediaType.BOOK + id = "0-1093989777" + } + + val searchresults = slub.parseSearchResults(json) + + assertEquals(1, searchresults.total_result_count) + assertThat(result1, samePropertyValuesAs(searchresults.results[0])) + } + @Test fun testParseResultById() { val json = JSONObject(readResource("/slub/search/simple-item.json")) diff --git a/opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json b/opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json new file mode 100644 index 000000000..0549bb010 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.json @@ -0,0 +1,111 @@ +{ + "numFound": 1, + "start": 0, + "docs": [ + { + "id": "0-1093989777", + "format": [ + "Book, E-Book" + ], + "title": "Tu en hagiois patros hēmōn Maximu tu homologetu Hapanta = S.P.N. Maximi Confessoris Opera omnia eruta, Latine transl., notisque ill. opera et studio Francisci Combefis. Adauxit Franciscus Oehler. Accurante et denuo recognoscente J.-P. Migne", + "author": [ + "Maximus Confessor", + "Combefis, François", + "Oehler, Franz", + "Migne, Jacques Paul" + ], + "creationDate": null, + "imprint": [ + "Lutetiae Parisiorum: Migne, 18XX" + ] + } + ], + "facets": { + "access_facet": { + "translation": "Zugang", + "values": { + "Local Holdings": { + "translation": "Lokale Bestände", + "values": 1 + } + } + }, + "format_de15": { + "translation": "Medientyp", + "values": { + "Book, E-Book": { + "translation": "Bücher", + "values": 1 + } + } + }, + "publishDateSort": { + "translation": "Erscheinungsjahr", + "values": [] + }, + "branch": { + "translation": "Standort", + "values": [] + }, + "language": { + "translation": "Sprache", + "values": { + "Ancient Greek": { + "translation": "Altgriechisch", + "values": 1 + }, + "Latin": { + "translation": "Latein", + "values": 1 + } + } + }, + "thema": { + "translation": "Fachgebiet", + "values": { + "Theologie und Religionswissenschaft": { + "translation": "Theologie und Religionswissenschaft", + "values": 1 + } + } + }, + "author": { + "translation": "Person/Institution", + "values": { + "Combefis, François": { + "translation": "Combefis, François", + "values": 1 + }, + "Maximus Confessor": { + "translation": "Maximus Confessor", + "values": 1 + }, + "Migne, Jacques Paul": { + "translation": "Migne, Jacques Paul", + "values": 1 + }, + "Oehler, Franz": { + "translation": "Oehler, Franz", + "values": 1 + } + } + }, + "facet_music_notation_de14": { + "translation": "musikalische Ausgabeform", + "values": [] + }, + "music_heading_browse": { + "translation": "musikalische Besetzung", + "values": [] + }, + "mega_collection": { + "translation": "Kollektion", + "values": { + "Verbunddaten SWB": { + "translation": "Verbunddaten SWB", + "values": 1 + } + } + } + } +} \ No newline at end of file From 63b2688d5a651bff97b5a34e1051f3ec4bf850b9 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Fri, 3 Jan 2020 16:50:23 +0100 Subject: [PATCH 26/55] Correctly process reserved inter-library loan items --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 16 +++- .../geeksfactory/opacclient/apis/SLUBTest.kt | 18 +++++ .../slub/account/account-ill_in_progress.json | 76 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index f3efcd571..5891a60bc 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -220,7 +220,7 @@ open class SLUB : OkHttpBaseApi() { internal fun parseAccountData(account: Account, json: JSONObject): AccountData { fun getReservations(items: JSONObject?): MutableList { - val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve", "ill") + val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" val reservationsList = mutableListOf() for (type in types) { @@ -237,6 +237,20 @@ open class SLUB : OkHttpBaseApi() { } } } + items?.optJSONArray("ill")?.let { + for (i in 0 until it.length()) { + reservationsList.add(it.getJSONObject(i).let { + ReservedItem().apply { + title = it.optString("Titel") + author = it.optString("Autor") + id = it.optString("Fernleih_ID") + branch = it.optString("Zweigstelle") + status = it.optString("Status_DESC") + } + }) + } + } + return reservationsList } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index ff8ff0e4c..04e0249f3 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -45,6 +45,24 @@ class SLUBAccountTest() : BaseHtmlTest() { assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) assertEquals("vorgemerkt", accountdata.lent[1].status) } + + @Test + fun testParseAccountDataIllInProgress() { + val json = JSONObject(readResource("/slub/account/account-ill_in_progress.json")) + val reserveditem = ReservedItem().apply { + title = "Kotlin" + author = "Szwillus, Karl" + id = "145073" + branch = "zell1" + status = "Bestellung ausgelöst" + } + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals(0, accountdata.lent.size) + assertEquals(1, accountdata.reservations.size) + assertThat(reserveditem, samePropertyValuesAs(accountdata.reservations[0])) + } } class SLUBSearchTest() : BaseHtmlTest() { diff --git a/opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json b/opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json new file mode 100644 index 000000000..6dd2bf74e --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json @@ -0,0 +1,76 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2020-03-31T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65375", + "X_date_of_last_issue_ext": "2019-12-28", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "items": { + "ill": [ + { + "Fernleih_ID": "145073", + "Titel": "Kotlin", + "Autor": "Szwillus, Karl", + "Datum_intern": "65381", + "Datum_extern": "2020-01-02T23:00:00Z", + "Datum_Ausleihe_intern": "65381", + "Datum_Ausleihe_extern": "2020-01-02T23:00:00Z", + "Zweigstelle": "zell1", + "Status": "2", + "Status_DESC": "Bestellung ausgelöst", + "Medientyp": "", + "History": [] + } + ] + }, + "fees": { + "amount": "0,00 EUR", + "subset": "open", + "amount_list": "0,00 EUR", + "paid_all": "0,00 EUR", + "paid_list": "0,00 EUR", + "fee_all": "0,00 EUR", + "fee_list": "0,00 EUR", + "topay_all": "0,00 EUR", + "topay_list": "0,00 EUR", + "overdue_paid": "0,00 EUR", + "overdue_fee": "0,00 EUR", + "ill_paid": "0,00 EUR", + "ill_fee": "0,00 EUR", + "other_paid": "0,00 EUR", + "other_fee": "0,00 EUR", + "fee": [] + }, + "arguments": { + "controller": "API", + "action": "account", + "username": "1234567" + } +} \ No newline at end of file From 3a5738c62abeeb33e6b7c2cff69161e500d5be19 Mon Sep 17 00:00:00 2001 From: StefRe Date: Fri, 3 Jan 2020 22:13:43 +0100 Subject: [PATCH 27/55] Make test of expiry date work independently of locale Format expected date value according to current locale (required for CI testing) --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 4 ++-- .../src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 5891a60bc..680f552ba 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -10,7 +10,7 @@ import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text import okhttp3.FormBody -import org.joda.time.LocalDateTime +import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter import org.json.JSONArray @@ -258,7 +258,7 @@ open class SLUB : OkHttpBaseApi() { return AccountData(account.id).apply { pendingFees = json.optJSONObject("fees")?.optString("topay_list") validUntil = json.optJSONObject("memberInfo")?.optString("expires") - ?.substring(0, 10)?.let { fmt.print(LocalDateTime(it)) } + ?.substring(0, 10)?.let { fmt.print(LocalDate(it)) } lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } ?.map { diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 04e0249f3..c09e8d844 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -4,6 +4,8 @@ import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs import de.geeksfactory.opacclient.objects.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -20,7 +22,7 @@ class SLUBAccountTest() : BaseHtmlTest() { val accountdata = slub.parseAccountData(Account(), json) assertEquals("1,23 EUR", accountdata.pendingFees) - assertEquals("31.03.20", accountdata.validUntil) + assertEquals(DateTimeFormat.shortDate().print(LocalDate("2020-03-31")), accountdata.validUntil) assertTrue(accountdata.lent.isEmpty()) assertTrue(accountdata.reservations.isEmpty()) } From 4ec3e242eb716a73523b9cef3ff1bb0375caee5d Mon Sep 17 00:00:00 2001 From: StefRe Date: Mon, 13 Jan 2020 21:12:23 +0100 Subject: [PATCH 28/55] implement cancel --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 680f552ba..badf65078 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -210,7 +210,12 @@ open class SLUB : OkHttpBaseApi() { } override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { - TODO("not implemented") + return try { + requestAccount(account, "delete", mapOf("tx_slubaccount_account[delete][0]" to media)) + OpacApi.CancelResult(OpacApi.MultiStepResult.Status.OK) + } catch (e: Exception) { + OpacApi.CancelResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } } override fun account(account: Account): AccountData { From 5e3b18941a5240be57f2dfe319f7e445e9276ea9 Mon Sep 17 00:00:00 2001 From: StefRe Date: Mon, 13 Jan 2020 21:19:56 +0100 Subject: [PATCH 29/55] Change search method from POST to GET If the search yields exactly one result then the POST method doesn't return a JSON object but the html catalogue detail web site of this item. Using GET handles this case correctly (returns JSON with one item). --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index badf65078..819ac6ce7 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -10,6 +10,7 @@ import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text import okhttp3.FormBody +import okhttp3.HttpUrl import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -69,21 +70,24 @@ open class SLUB : OkHttpBaseApi() { } override fun searchGetPage(page: Int): SearchRequestResult { - val queryfbb = FormBody.Builder() - .add("type", "1369315142") - .add("tx_find_find[format]", "data") - .add("tx_find_find[data-format]", "app") - .add("tx_find_find[page]", page.toString()) + if (query.size <= 4) { + throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) + } + val queryUrlB = HttpUrl.get("$baseurl/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app") + .newBuilder() + .addQueryParameter("tx_find_find[page]", page.toString()) for (sq in query) { if (sq.value.isNotEmpty()) { - queryfbb.add("tx_find_find[q][${sq.key}]", sq.value) + queryUrlB.addQueryParameter("tx_find_find[q][${sq.key}]", sq.value) } } - val queryfb = queryfbb.build() - if (queryfb.size() <= 4) - throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) - val json = JSONObject(httpPost(baseurl, queryfb, ENCODING)) - return parseSearchResults(json) + try { + return parseSearchResults(JSONObject(httpGet(queryUrlB.build().toString(), ENCODING))) + } catch (e: JSONException) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_WITH_DESCRIPTION, + "search returned malformed JSON object: ${e.message}")) + } } internal fun parseSearchResults(json: JSONObject): SearchRequestResult{ From 1d45d9ea869ed6f378587bc5e1d36164faa5d1ef Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 15 Jan 2020 21:40:26 +0100 Subject: [PATCH 30/55] Add MIT license header as requested at https://github.com/opacapp/opacclient/wiki --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 819ac6ce7..9268c831f 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -1,3 +1,24 @@ +/** + * Copyright (C) 2020 by Steffen Rehberg under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ package de.geeksfactory.opacclient.apis import de.geeksfactory.opacclient.i18n.StringProvider From 954149ce890f2ded8222c99469b71015dc8e393a Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 15 Jan 2020 22:22:04 +0100 Subject: [PATCH 31/55] Change all http requests methods from POST to GET As of Jan 15, 2020, all account POST requests get redirected to the web catalogue login page and POST requests for all search requests get redirected to the html result pages of the web catalogue. --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 9268c831f..68445fba3 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -30,7 +30,6 @@ import de.geeksfactory.opacclient.searchfields.TextSearchField import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text -import okhttp3.FormBody import okhttp3.HttpUrl import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat @@ -135,11 +134,16 @@ open class SLUB : OkHttpBaseApi() { } override fun getResultById(id: String, homebranch: String?): DetailedItem { - val detailfb = FormBody.Builder() - .add("type", "1369315142") - .add("tx_find_find[format]", "data") - .add("tx_find_find[data-format]", "app") - val json = JSONObject(httpPost("$baseurl/id/$id/", detailfb.build(), ENCODING)) + val json: JSONObject + try { + json = JSONObject(httpGet( + "$baseurl/id/$id/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app", + ENCODING)) + } catch (e: JSONException) { + throw OpacApi.OpacErrorException(stringProvider.getFormattedString( + StringProvider.UNKNOWN_ERROR_WITH_DESCRIPTION, + "search returned malformed JSON object: ${e.message}")) + } return parseResultById(id, json) } @@ -316,17 +320,16 @@ open class SLUB : OkHttpBaseApi() { } private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { - val formBody = FormBody.Builder() - .add("type", "1") - .add("tx_slubaccount_account[controller]", "API") - .add("tx_slubaccount_account[action]", action) - .add("tx_slubaccount_account[username]", account.name) - .add("tx_slubaccount_account[password]", account.password) + val queryUrlB = HttpUrl.get("$baseurl/mein-konto/?type=1&tx_slubaccount_account[controller]=API") + .newBuilder() + .addQueryParameter("tx_slubaccount_account[action]", action) + .addQueryParameter("tx_slubaccount_account[username]", account.name) + .addQueryParameter("tx_slubaccount_account[password]", account.password) parameters?.map { - formBody.add(it.key, it.value) + queryUrlB.addQueryParameter(it.key, it.value) } try { - return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { + return JSONObject(httpGet(queryUrlB.build().toString(), ENCODING)).also { if (it.optInt("status") != 1) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, From 35d9b77061c1831de285b9de524a9e03cb93bf74 Mon Sep 17 00:00:00 2001 From: StefRe Date: Thu, 16 Jan 2020 23:14:01 +0100 Subject: [PATCH 32/55] Add remaining details to DetailedItem and fix reserved and lent items --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 73 +++++- .../opacclient/i18n/StringProvider.java | 5 + .../geeksfactory/opacclient/apis/SLUBTest.kt | 159 +++++++++++- .../resources/slub/search/item-fotothek.json | 43 ++++ .../resources/slub/search/item-links.json | 82 ++++++ .../search/item-links_general_no_label.json | 44 ++++ .../slub/search/item-multiple_parts_item.json | 76 ++++++ ...tem-with_umlaute_in_title_and_volumes.json | 233 ++++++++++++++++++ .../main/res/values-de/strings_api_errors.xml | 5 + .../main/res/values/strings_api_errors.xml | 5 + 10 files changed, 710 insertions(+), 15 deletions(-) create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-fotothek.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-links.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 68445fba3..2ec96b036 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -76,7 +76,10 @@ open class SLUB : OkHttpBaseApi() { "identifier" to "ISBN", "language" to "Sprache", "subject" to "Schlagwörter", - "description" to "Beschreibung" + "description" to "Beschreibung", + "linksRelated" to "Info", + "linksAccess" to "Zugang", + "linksGeneral" to "Link" ) override fun init(library: Library, factory: HttpClientFactory) { @@ -182,7 +185,10 @@ open class SLUB : OkHttpBaseApi() { val arrayItem = v.get(it) when (arrayItem) { is String -> arrayItem - is JSONObject -> arrayItem.optString("title") + is JSONObject -> arrayItem.optString("title").also { + // if item is part of multiple collections, collectionsId holds the last one + collectionId = arrayItem.optString(("id"), null) + } else -> null } }.joinToString("; ") @@ -196,6 +202,27 @@ open class SLUB : OkHttpBaseApi() { addDetail(Detail(fieldCaptions[key], value)) } } + json.optString("thumbnail")?.run { + if (this != "") { + cover = this + } + } + // links and references + for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ + val linkArray = json.optJSONArray(link) + linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map{ + // assuming that only on of material, note or hostlabel is set + val key = with(it.optString("material") + it.optString("note") + it.optString("hostLabel")) { + if (isEmpty()) fieldCaptions[link] else this + } + addDetail(Detail( key, it.optString("uri"))) + } + } + json.optJSONArray("references").run { 0.until(length()).map { optJSONObject(it) } }.map{ + // TODO: usually links to old SLUB catalogue, does it make sense to add the link? + addDetail(Detail( it.optString("text"), "${it.optString("name")} (${it.optString("target")})")) + } + // copies val cps = json.opt("copies") if (cps is JSONArray) { getCopies(cps, dateFormat)?.let { copies = it } @@ -209,10 +236,13 @@ open class SLUB : OkHttpBaseApi() { } copies = copiesList } - // TODO: volumes - // TODO: collectionid - // TODO: add linksAccess as detail (uri & hostLabel, note?, material?) - // TODO: add other links (links, linksRelated, linksGeneral) as details? + // volumes + volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { + 0.until(length()).map { optJSONObject(it) }?.map { + Volume(it.optString("id"), + "${it.optString("part")} ${Parser.unescapeEntities(it.optString("name"),false)}") + } + } ?: emptyList() } } @@ -253,6 +283,7 @@ open class SLUB : OkHttpBaseApi() { } internal fun parseAccountData(account: Account, json: JSONObject): AccountData { + val fmt = DateTimeFormat.shortDate() fun getReservations(items: JSONObject?): MutableList { val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" @@ -264,8 +295,25 @@ open class SLUB : OkHttpBaseApi() { ReservedItem().apply { title = it.optString("about") author = it.optJSONArray("X_author")?.optString(0) + //id = it.optString("label") // TODO: get details from here via /bc --> redirects to /id, from there get the proper id format = it.optString("X_medientyp") - status = it.optInt("X_queue_number").let { "Pos. $it" } + status = when(type){ // TODO: maybe we need time (LocalDateTime) too make an educated guess on actual ready date for stack requests + "hold" -> stringProvider.getFormattedString(StringProvider.HOLD, + fmt.print(LocalDate(it.optString("X_date_reserved").substring(0, 10)))) + "request_ready" -> stringProvider.getFormattedString(StringProvider.REQUEST_READY, + fmt.print(LocalDate(it.optString("X_date_requested").substring(0, 10)))) + "readingroom" -> stringProvider.getFormattedString(StringProvider.READINGROOM, + fmt.print(LocalDate(it.optString("X_date_provided").substring(0, 10)))) + "request_progress" -> stringProvider.getFormattedString(StringProvider.REQUEST_PROGRESS, + fmt.print(LocalDate(it.optString("X_date_requested").substring(0, 10)))) + "reserve" -> stringProvider.getFormattedString(StringProvider.RESERVED_POS, + it.optInt("X_queue_number")) + else -> null + } + branch = it.optString("X_pickup_desc", null) + if (type == "reserve") { + cancelData = "${it.optString("label")}_${it.getInt("X_delete_number")}" + } } }) } @@ -277,7 +325,10 @@ open class SLUB : OkHttpBaseApi() { ReservedItem().apply { title = it.optString("Titel") author = it.optString("Autor") - id = it.optString("Fernleih_ID") + //id = it.optString("Fernleih_ID") --> this id is of no use whatsoever + it.optString("Medientyp")?.run { + if (length > 0) format = this + } branch = it.optString("Zweigstelle") status = it.optString("Status_DESC") } @@ -288,12 +339,11 @@ open class SLUB : OkHttpBaseApi() { return reservationsList } - val fmt = DateTimeFormat.shortDate() return AccountData(account.id).apply { pendingFees = json.optJSONObject("fees")?.optString("topay_list") validUntil = json.optJSONObject("memberInfo")?.optString("expires") ?.substring(0, 10)?.let { fmt.print(LocalDate(it)) } - lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans (need example) + lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans? (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } ?.map { LentItem().apply { @@ -301,13 +351,14 @@ open class SLUB : OkHttpBaseApi() { author = it.optJSONArray("X_author")?.optString(0) setDeadline(it.optString("X_date_due")) format = it.optString("X_medientyp") + //id = it.optString("label") // TODO: get details from here via /bc --> redirects to /id, from there get the proper id barcode = it.optString("X_barcode") status = when { it.optInt("renewals") == 2 -> "2x verlängert" it.optInt("X_is_reserved") != 0 -> "vorgemerkt" else -> null } - isRenewable = if (it.optInt("X_is_renewable") == 1) { + isRenewable = if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items prolongData = barcode true } else { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java index cee8bee79..999c624f6 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java @@ -87,6 +87,11 @@ public interface StringProvider { String PLEASE_CHANGE_PASSWORD = "please_change_password"; String PROLONG_ALL_NOT_POSSIBLE = "prolong_all_not_possible"; String PROLONG_ALL_NO_ITEMS = "prolong_all_no_items"; + String HOLD = "hold"; + String REQUEST_READY = "request_ready"; + String READINGROOM = "readingroom"; + String REQUEST_PROGRESS = "request_progress"; + String RESERVED_POS = "reserved_pos"; /** * Returns the translated string identified by identifier diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index c09e8d844..8b58f10df 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -1,20 +1,47 @@ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs +import de.geeksfactory.opacclient.i18n.StringProvider +import de.geeksfactory.opacclient.i18n.StringProvider.* import de.geeksfactory.opacclient.objects.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Test +private class TestStringProvider : StringProvider { + override fun getString(identifier: String?): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getFormattedString(identifier: String?, vararg args: Any?): String { + return when (identifier) { + RESERVED_POS -> String.format("vorgemerkt, Pos. %s", *args) + HOLD -> String.format("liegt seit %s bereit", *args) + REQUEST_READY -> String.format("seit %s abholbereit (Magazinbestellung)", *args) + else -> identifier!! + } + } + + override fun getQuantityString(identifier: String?, count: Int, vararg args: Any?): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getMediaTypeName(mediaType: SearchResult.MediaType?): String { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} class SLUBAccountTest() : BaseHtmlTest() { var slub = SLUB() + init { + slub.stringProvider = TestStringProvider() + } + @Test fun testParseEmptyAccountData() { val json = JSONObject(readResource("/slub/account/empty-account.json")) @@ -30,22 +57,52 @@ class SLUBAccountTest() : BaseHtmlTest() { @Test fun testParseAccountData() { val json = JSONObject(readResource("/slub/account/account.json")) + val fmt = DateTimeFormat.shortDate() val lentitem1 = LentItem().apply { title = "¬Der¬ neue Kosmos-Baumführer" author = "Bachofer, Mark" setDeadline("2019-06-03") format = "B" + //id = "31626878" barcode = "31626878" isRenewable = true prolongData = barcode } - + val reserveditem1 = ReservedItem().apply { + // reserve + title = "Pareys Buch der Bäume" + author = "Mitchell, Alan" + format = "B" + //id = "30963742" + status = "vorgemerkt, Pos. 1" + cancelData = "30963742_1" + } + val reserveditem2 = ReservedItem().apply { + // hold + title = "Welcher Baum ist das?" + author = "Mayer, Joachim ¬[VerfasserIn]¬" + format = "B" + //id = "34778398" + branch = "ZwB Forstwissenschaft" + status = String.format("liegt seit %s bereit", fmt.print(LocalDate("2019-05-10"))) + } + val reserveditem3 = ReservedItem().apply { + // request ready + title = "Englische Synonyme als Fehlerquellen" + author = "Meyer, Jürgen" + format = "B" + //id = "20550495" + branch = "Zentralbibliothek Ebene 0 SB-Regal" + status = String.format("seit %s abholbereit (Magazinbestellung)", fmt.print(LocalDate("2019-05-04"))) + } val accountdata = slub.parseAccountData(Account(), json) assertEquals(2, accountdata.lent.size) assertEquals(3, accountdata.reservations.size) assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) assertEquals("vorgemerkt", accountdata.lent[1].status) + assertThat(accountdata.reservations, hasItems(sameBeanAs(reserveditem1), + sameBeanAs(reserveditem2), sameBeanAs(reserveditem3))) } @Test @@ -54,7 +111,7 @@ class SLUBAccountTest() : BaseHtmlTest() { val reserveditem = ReservedItem().apply { title = "Kotlin" author = "Szwillus, Karl" - id = "145073" + //id = "145073" branch = "zell1" status = "Bestellung ausgelöst" } @@ -123,6 +180,7 @@ class SLUBSearchTest() : BaseHtmlTest() { addDetail(Detail("ISBN", "3446228241; 3446404694; 9783446404694; 9783446228245")) addDetail(Detail("Sprache", "Deutsch")) addDetail(Detail("Schlagwörter", "Quellcode; Softwaretest; JUnit")) + addDetail(Detail("Inhaltsverzeichnis", "http://d-nb.info/970689268/04")) id = "0-1182402208" copies = arrayListOf(Copy().apply { barcode = "31541466" @@ -131,6 +189,7 @@ class SLUBSearchTest() : BaseHtmlTest() { status = "Ausleihbar" shelfmark = "ST 233 H939" }) + collectionId = "0-1183957874" } val item = slub.parseResultById(json.getString("id"), json) @@ -164,4 +223,96 @@ class SLUBSearchTest() : BaseHtmlTest() { // the copies arrays may occur in any order assertThat(item.copies, hasItems(sameBeanAs(copyFirst), sameBeanAs(copyLast))) } + + @Test + fun testParseResultByIdMultipleParts() { + val json = JSONObject(readResource("/slub/search/item-multiple_parts_item.json")) + val volumes = listOf( + Volume("0-1453040935", "[3]: Principles of digital image processing"), + Volume("0-1347927328", "[2]: Principles of digital image processing"), + Volume("0-1347930884", "[1]: Principles of digital image processing") + ) + + // is part of "Undergraduate topics in computer science" but no id (--> collectionid) given + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(volumes, sameBeanAs(item.volumes)) + assertNull(item.collectionId) + } + + @Test + fun testParseResultByIdUmlaute() { + val json = JSONObject(readResource("/slub/search/item-with_umlaute_in_title_and_volumes.json")) + val volume = Volume("0-1149529121", "(inse,5): in 6 Bänden") + + val item = slub.parseResultById(json.getString("id"), json) + + assertEquals("Urania-Tierreich: in 6 Bänden", item.title) + assertThat(item.volumes, hasItem(sameBeanAs(volume))) + } + + @Test + fun testParseResultByIdThumbnail() { + val json = JSONObject(readResource("/slub/search/item-fotothek.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Foto")) + addDetail(Detail("Titel", "Maya")) + title = "Maya" + addDetail(Detail("Sprache", "Kein linguistischer Inhalt")) + addDetail(Detail("Schlagwörter", "Skulptur; Statue; Ortskatalog zur Kunst und Architektur")) + id = "dswarm-67-b2FpOmRldXRzY2hlZm90b3RoZWsuZGU6YTg0NTA6Om9ianwzMzA1NTgxMHxkZl9oYXVwdGthdGFsb2dfMDEwMDMzNg" + cover = "http://fotothek.slub-dresden.de/thumbs/df_hauptkatalog_0100336.jpg" + addDetail(Detail("In der Deutschen Fotothek ansehen", "http://www.deutschefotothek.de/obj33055810.html")) + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } + + @Test + fun testParseResultByIdLinks() { + val json = JSONObject(readResource("/slub/search/item-links.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Buch")) + addDetail(Detail("Titel", "JUnit-Profiwissen: effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java")) + title = "JUnit-Profiwissen: effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java" + addDetail(Detail("Beteiligte", "Tamm, Michael [Autor/In]")) + addDetail(Detail("Erschienen", "Heidelberg dpunkt.Verl. 2013 ")) + addDetail(Detail("ISBN", "3864900204; 9783864900204")) + addDetail(Detail("Sprache", "Deutsch")) + addDetail(Detail("Schlagwörter", "Java; JUnit")) + addDetail(Detail("Beschreibung", "Literaturverz. S. 351")) + id = "0-727434322" + addDetail(Detail("Inhaltsverzeichnis","http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf")) + addDetail(Detail("Inhaltstext","http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm")) + addDetail(Detail("Zugang zur Ressource (via ProQuest Ebook Central)", "http://wwwdb.dbod.de/login?url=http://slub.eblib.com/patron/FullRecord.aspx?p=1575685")) + addDetail(Detail("Online-Ausgabe", "Tamm, Michael: JUnit-Profiwissen (SLUB)")) + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } + + @Test + fun testParseResultByIdLinksGeneralNoLabel() { + val json = JSONObject(readResource("/slub/search/item-links_general_no_label.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "ElectronicThesis")) + addDetail(Detail("Titel", "A Study of Classic Maya Rulership")) + title = "A Study of Classic Maya Rulership" + addDetail(Detail("Sprache", "Englisch")) + addDetail(Detail("Schlagwörter", "Archaeology; Latin American history; Ancient history; Native American studies")) + id = "ai-34-b2FpOnBxZHRvYWkucHJvcXVlc3QuY29tOjM0Nzc2Mzg" + addDetail(Detail("Link", "http://pqdtopen.proquest.com/#viewpdf?dispub=3477638")) + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } } \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-fotothek.json b/opacclient/libopac/src/test/resources/slub/search/item-fotothek.json new file mode 100644 index 000000000..bcdcb7894 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-fotothek.json @@ -0,0 +1,43 @@ +{ + "record": { + "format": "Foto", + "title": "Maya", + "contributor": [], + "publisher": [], + "ispartof": [], + "identifier": "", + "language": [ + "Kein linguistischer Inhalt" + ], + "subject": [ + "Skulptur", + "Statue", + "Ortskatalog zur Kunst und Architektur" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "dswarm-67-b2FpOmRldXRzY2hlZm90b3RoZWsuZGU6YTg0NTA6Om9ianwzMzA1NTgxMHxkZl9oYXVwdGthdGFsb2dfMDEwMDMzNg", + "oa": 0, + "thumbnail": "http://fotothek.slub-dresden.de/thumbs/df_hauptkatalog_0100336.jpg", + "links": [ + { + "uri": "http://wwwdb.dbod.de/login?url=http://www.deutschefotothek.de/obj33055810.html", + "note": "In der Deutschen Fotothek ansehen" + } + ], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [ + { + "uri": "http://www.deutschefotothek.de/obj33055810.html", + "hostLabel": "In der Deutschen Fotothek ansehen", + "note": "", + "material": "" + } + ], + "references": [], + "copies": [], + "parts": {} +} diff --git a/opacclient/libopac/src/test/resources/slub/search/item-links.json b/opacclient/libopac/src/test/resources/slub/search/item-links.json new file mode 100644 index 000000000..960326916 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-links.json @@ -0,0 +1,82 @@ +{ + "record": { + "format": "Buch", + "title": "JUnit-Profiwissen: effizientes Arbeiten mit der Standardbibliothek für automatisierte Tests in Java", + "contributor": [ + "Tamm, Michael [Autor/In]" + ], + "publisher": [ + "Heidelberg dpunkt.Verl. 2013 " + ], + "ispartof": [], + "identifier": [ + "3864900204", + "9783864900204" + ], + "language": [ + "Deutsch" + ], + "subject": [ + "Java", + "JUnit" + ], + "description": [ + "Literaturverz. S. 351" + ], + "status": "", + "rvk": "" + }, + "id": "0-727434322", + "oa": 0, + "thumbnail": "", + "links": [ + { + "uri": "http://slub.eblib.com/patron/FullRecord.aspx?p=1575685", + "note": "Zugang zur Ressource (via ProQuest Ebook Central)", + "material": "" + }, + { + "uri": "http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf", + "note": "Auf PDF zugreifen", + "material": "Inhaltsverzeichnis" + }, + { + "uri": "http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm", + "note": "", + "material": "Inhaltstext" + } + ], + "linksRelated": [ + { + "uri": "http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf", + "hostLabel": "", + "note": "", + "material": "Inhaltsverzeichnis" + }, + { + "uri": "http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm", + "hostLabel": "", + "note": "", + "material": "Inhaltstext" + } + ], + "linksAccess": [ + { + "uri": "http://wwwdb.dbod.de/login?url=http://slub.eblib.com/patron/FullRecord.aspx?p=1575685", + "hostLabel": "Zugang zur Ressource (via ProQuest Ebook Central)", + "note": "", + "material": "" + } + ], + "linksGeneral": [], + "references": [ + { + "text": "Online-Ausgabe", + "link": "http://slubdd.de/katalog?libero_mab216187885", + "name": "Tamm, Michael: JUnit-Profiwissen", + "target": "SLUB" + } + ], + "copies": [], + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json b/opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json new file mode 100644 index 000000000..ecdaca72c --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-links_general_no_label.json @@ -0,0 +1,44 @@ +{ + "record": { + "format": "ElectronicThesis", + "title": "A Study of Classic Maya Rulership", + "contributor": [], + "publisher": [], + "ispartof": [], + "identifier": "", + "language": [ + "Englisch" + ], + "subject": [ + "Archaeology", + "Latin American history", + "Ancient history", + "Native American studies" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "ai-34-b2FpOnBxZHRvYWkucHJvcXVlc3QuY29tOjM0Nzc2Mzg", + "oa": 0, + "thumbnail": "", + "links": [ + { + "uri": "http://wwwdb.dbod.de/login?url=http://pqdtopen.proquest.com/#viewpdf?dispub=3477638", + "note": "http://wwwdb.dbod.de/login?url=http://pqdtopen.proquest.com/#viewpdf?dispub=3477638" + } + ], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [ + { + "uri": "http://pqdtopen.proquest.com/#viewpdf?dispub=3477638", + "hostLabel": "", + "note": "", + "material": "" + } + ], + "references": [], + "copies": [], + "parts": {} +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json b/opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json new file mode 100644 index 000000000..3307283d4 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-multiple_parts_item.json @@ -0,0 +1,76 @@ +{ + "record": { + "format": "Buch", + "title": "Principles of digital image processing", + "contributor": [ + "Burger, Wilhelm", + "Burge, Mark James [Autor/In]" + ], + "publisher": [ + "London Springer 20XX " + ], + "ispartof": [ + { + "title": "Undergraduate topics in computer science" + } + ], + "identifier": "", + "language": [ + "Englisch" + ], + "subject": "", + "description": "", + "status": "", + "rvk": "" + }, + "id": "0-1393168353", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [], + "parts": { + "title": "Mehrbändiges Werk", + "records": [ + { + "author": [ + "Burger, Wilhelm", + "Burge, Mark J." + ], + "author2": [], + "part": "[3]:", + "id": "0-1453040935", + "name": "Principles of digital image processing", + "imprint": "London, Springer, 2013", + "sequence": "[3]" + }, + { + "author": [ + "Burger, Wilhelm", + "Burge, Mark James" + ], + "author2": [], + "part": "[2]:", + "id": "0-1347927328", + "name": "Principles of digital image processing", + "imprint": "London, Springer, 2009", + "sequence": "[2]" + }, + { + "author": [ + "Burger, Wilhelm", + "Burge, Mark James" + ], + "author2": [], + "part": "[1]:", + "id": "0-1347930884", + "name": "Principles of digital image processing", + "imprint": "London, Springer, 2009", + "sequence": "[1]" + } + ] + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json b/opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json new file mode 100644 index 000000000..bdc76f97f --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-with_umlaute_in_title_and_volumes.json @@ -0,0 +1,233 @@ +{ + "record": { + "format": "Buch", + "title": "Urania-Tierreich: in 6 Bänden", + "contributor": [], + "publisher": [ + "Leipzig Jena Berlin Urania-Verl. 19XX- " + ], + "ispartof": [], + "identifier": "", + "language": [ + "Deutsch" + ], + "subject": [ + "Tiere" + ], + "description": "", + "status": "", + "rvk": "" + }, + "id": "0-112458708X", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [], + "parts": { + "title": "Mehrbändiges Werk", + "records": [ + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse,5):", + "id": "0-1149529121", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1989", + "sequence": "inse,5" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": ":", + "id": "0-1124595643", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1986", + "sequence": "saug,5" + }, + { + "author": [], + "author2": [ + "Gruner, Hans-Eckhard" + ], + "part": "(wirb,1,3):", + "id": "0-1149528753", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1981", + "sequence": "wirb,1,3" + }, + { + "author": [], + "author2": [ + "Crome, Wolfgang" + ], + "part": "(wirb,2,3):", + "id": "0-1149528885", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1979", + "sequence": "wirb,2,3" + }, + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse,4):", + "id": "0-1162094052", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1978", + "sequence": "inse,4" + }, + { + "author": [], + "author2": [ + "Mauersberger, Gottfried" + ], + "part": "(voge):", + "id": "0-1129107655", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1977", + "sequence": "voge" + }, + { + "author": [], + "author2": [ + "Deckert, Kurt" + ], + "part": "(fisc,4):", + "id": "0-1149529466", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1976", + "sequence": "fisc,4" + }, + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse, 2./3.):", + "id": "0-1124590285", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1974", + "sequence": "inse, 2./3." + }, + { + "author": [], + "author2": [ + "Deckert, Kurt" + ], + "part": "(fisc,3):", + "id": "0-1162153717", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1973", + "sequence": "fisc,3" + }, + { + "author": [], + "author2": [ + "Mauersberger, Gottfried" + ], + "part": "(voge,3):", + "id": "0-1162188383", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1972", + "sequence": "voge,3" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": "(saug,3):", + "id": "0-112459082X", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1972", + "sequence": "saug,3" + }, + { + "author": [], + "author2": [ + "Mauersberger, Gottfried" + ], + "part": "(voge):", + "id": "0-1124587314", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1969", + "sequence": "voge" + }, + { + "author": [], + "author2": [ + "Crome, Wolfgang" + ], + "part": "(wirb,2):", + "id": "0-1124588604", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1969", + "sequence": "wirb,2" + }, + { + "author": [], + "author2": [ + "Günther, Kurt K." + ], + "part": "(inse):", + "id": "0-1124590048", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1968", + "sequence": "inse" + }, + { + "author": [], + "author2": [ + "Deckert, Kurt" + ], + "part": "(fisc):", + "id": "0-1124587748", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1967", + "sequence": "fisc" + }, + { + "author": [], + "author2": [ + "Crome, Wolfgang" + ], + "part": "(wirb,1):", + "id": "0-112458983X", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1967", + "sequence": "wirb,1" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": "(saug):", + "id": "0-1124591036", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1967", + "sequence": "saug" + }, + { + "author": [], + "author2": [ + "Petzsch, Hans" + ], + "part": "(saug,1):", + "id": "0-117225883X", + "name": "in 6 Bänden", + "imprint": "Leipzig, Jena, Berlin, Urania-Verl., 1966", + "sequence": "saug,1" + } + ] + } +} diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index 445bcfa82..34c5962e6 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -87,4 +87,9 @@ Die Bibliothek fordert Sie auf, Ihr Passwort zu ändern. Bitte loggen Sie sich dazu auf der Bibliothekswebsite ein, um das Passwort zu ändern, und geben dann das neue Passwort in der App ein. Eine Verlängerung aller Medien ist momentan nicht möglich. Es sind keine Medien verlängerbar. + liegt seit %s bereit + seit %s abholbereit (Magazinbestellung) + liegt seit %s zur Benutzung in Bibliothek bereit + Magazinbestellung seit %s in Arbeit + vorgemerkt, Pos. %s diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index 09f494060..ede468d2c 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -88,4 +88,9 @@ The library is asking you to change your password. Please first log in on the library website to change your password, and then enter the new password in the app. Renewing all items is not possible at the moment. No items are renewable. + on hold since %s + ready for pickup since %s (stack request) + ready since %s for usage in library + stack request in progress since %s + reserved, pos. %s From c52e6657e6ed1260189da7d97f6972b580ab16cf Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 19 Jan 2020 19:10:38 +0100 Subject: [PATCH 33/55] Add SUPPORT_FLAG_ENDLESS_SCROLLING flag to prevent reloading the page the selected result is located on before calling getResultById. --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 2ec96b036..1051d7b9f 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -403,7 +403,7 @@ open class SLUB : OkHttpBaseApi() { } override fun getSupportFlags(): Int { - return 0 + return OpacApi.SUPPORT_FLAG_ENDLESS_SCROLLING } override fun getSupportedLanguages(): Set? { From 3b8f6638bd2c1842774fa7a7e141fb4385c9cdd7 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 19 Jan 2020 20:02:32 +0100 Subject: [PATCH 34/55] Partly revert d18931b32d337cfeff279427e175c0c13bd0aea5 As of Jan 17, 2020, POST requests are accepted again. So we change the account requests back to POST for security reasons (see recommendation https://tools.ietf.org/html/rfc2616#section-15.1.3) --- .../java/de/geeksfactory/opacclient/apis/SLUB.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 1051d7b9f..ab2191528 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -30,6 +30,7 @@ import de.geeksfactory.opacclient.searchfields.TextSearchField import de.geeksfactory.opacclient.utils.get import de.geeksfactory.opacclient.utils.html import de.geeksfactory.opacclient.utils.text +import okhttp3.FormBody import okhttp3.HttpUrl import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat @@ -371,16 +372,17 @@ open class SLUB : OkHttpBaseApi() { } private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { - val queryUrlB = HttpUrl.get("$baseurl/mein-konto/?type=1&tx_slubaccount_account[controller]=API") - .newBuilder() - .addQueryParameter("tx_slubaccount_account[action]", action) - .addQueryParameter("tx_slubaccount_account[username]", account.name) - .addQueryParameter("tx_slubaccount_account[password]", account.password) + val formBody = FormBody.Builder() + .add("type", "1") + .add("tx_slubaccount_account[controller]", "API") + .add("tx_slubaccount_account[action]", action) + .add("tx_slubaccount_account[username]", account.name) + .add("tx_slubaccount_account[password]", account.password) parameters?.map { - queryUrlB.addQueryParameter(it.key, it.value) + formBody.add(it.key, it.value) } try { - return JSONObject(httpGet(queryUrlB.build().toString(), ENCODING)).also { + return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { if (it.optInt("status") != 1) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, From c5b2b7bca7d037ef742a90a4994b5cbf7e8d8226 Mon Sep 17 00:00:00 2001 From: StefRe Date: Thu, 23 Jan 2020 22:49:28 +0100 Subject: [PATCH 35/55] Process boolean status values in account request results and add test for requestAccount --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 4 +- .../geeksfactory/opacclient/apis/SLUBTest.kt | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index ab2191528..24e0d5653 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -371,7 +371,7 @@ open class SLUB : OkHttpBaseApi() { } } - private fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { + internal fun requestAccount(account: Account, action: String, parameters: Map? = null): JSONObject { val formBody = FormBody.Builder() .add("type", "1") .add("tx_slubaccount_account[controller]", "API") @@ -383,7 +383,7 @@ open class SLUB : OkHttpBaseApi() { } try { return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { - if (it.optInt("status") != 1) { + if (!(it.optInt("status") == 1 || it.optBoolean("status"))) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, it.optString("message", "error requesting account data"))) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 8b58f10df..6afa6af6d 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -3,6 +3,7 @@ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.i18n.StringProvider.* +import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* @@ -10,7 +11,13 @@ import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.json.JSONObject import org.junit.Assert.* +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Matchers +import org.mockito.Mockito private class TestStringProvider : StringProvider { override fun getString(identifier: String?): String { @@ -315,4 +322,55 @@ class SLUBSearchTest() : BaseHtmlTest() { assertThat(item, sameBeanAs(expected).ignoring("details")) assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) } +} + +@RunWith(Parameterized::class) +class SLUBAccountMockTest(private val response: String, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + } + }, HttpClientFactory("test")) + } + private val account = Account().apply { + name = "x" + password = "x" + } + + @JvmField + @Rule + var thrown: ExpectedException = ExpectedException.none() + + @Test + fun testCheckAccountData() { + Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) + if (expectedException != null) { + thrown.expect(expectedException) + thrown.expectMessage(expectedExceptionMsg) + } + + slub.requestAccount(account, "", null) + } + + companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = listOf( + // validate: status as string + arrayOf("{\"status\":\"1\",\"message\":\"credentials_are_valid\"}", null, null), + arrayOf("{\"message\":\"error_credentials_invalid\",\"arguments\":{\"controller\":\"API\",\"action\":\"validate\",\"username\":\"123456\"},\"status\":\"-1\"}", OpacApi.OpacErrorException::class.java, "error_credentials_invalid"), + // POST not accepted, malformed request, e.g. invalid action + arrayOf(".", OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), + // delete: status as int or string + arrayOf("{\"status\":1,\"message\":\"Reservation deleted\"}", null, null), + arrayOf("{\"status\":\"-1\",\"message\":\"Item not reserved\"}", OpacApi.OpacErrorException::class.java, "Item not reserved"), + // pickup: status as boolean + arrayOf("{\"status\":true,\"message\":\"n\\/a\"}", null, null), + arrayOf("{\"status\":false,\"message\":\"Ungültige Barcodenummer\"}", OpacApi.OpacErrorException::class.java, "Ungültige Barcodenummer") + ) + } } \ No newline at end of file From 213fff62e2bf5391d68dd1179cbd2d36af10d390 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 29 Jan 2020 21:18:59 +0100 Subject: [PATCH 36/55] Implement reservation and tests for it - minor fix to SLUBAccountMockTest - combine all SLUB tests into test suite --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 115 +++++- .../opacclient/i18n/StringProvider.java | 1 + .../geeksfactory/opacclient/apis/SLUBTest.kt | 369 +++++++++++++++++- .../slub/search/item-for-reserve&request.json | 98 +++++ .../main/res/values-de/strings_api_errors.xml | 1 + .../main/res/values/strings_api_errors.xml | 1 + 6 files changed, 570 insertions(+), 15 deletions(-) create mode 100644 opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 24e0d5653..a3b9dc9a5 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -83,6 +83,21 @@ open class SLUB : OkHttpBaseApi() { "linksGeneral" to "Link" ) + private val ACTION_COPY = OpacApi.MultiStepResult.ACTION_USER + 1 + private val pickupPoints = mapOf( + "a01" to "Zentralbibliothek Ebene 0 SB-Regal", + "a02" to "Zentralbibliothek, Ebene -1, IP Musik Mediathek", + "a03" to "Zentralbibliothek, Ebene -1, Lesesaal Sondersammlungen", + "a05" to "Zentralbibliothek, Ebene -2, Lesesaal Kartensammlung", + "a06" to "ZwB Rechtswissenschaft", + "a07" to "ZwB Erziehungswissenschaft", + "a08" to "ZwB Medizin", + "a09" to "ZwB Forstwissenschaft", + "a10" to "Bereichsbibliothek Drepunct", + "a13" to "Zentralbibliothek, Servicetheke", + "a14" to "Zentralbibliothek, Ebene -1, SB-Regal Zeitungen" + ) + override fun init(library: Library, factory: HttpClientFactory) { super.init(library, factory) baseurl = library.data.getString("baseurl") @@ -153,6 +168,7 @@ open class SLUB : OkHttpBaseApi() { internal fun parseResultById(id:String, json: JSONObject): DetailedItem { val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") + var hasReservableCopies = false fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = copiesArray.run { 0.until(length()).map { optJSONObject(it) } } .map { @@ -167,8 +183,14 @@ open class SLUB : OkHttpBaseApi() { returnDate = df.parseLocalDate(this) } } + // stack requests and reservations for items on loan are both handled as "reservations" in libopac + if (it.getString("bestellen") == "1") { + resInfo = "stackRequest\t$barcode" + hasReservableCopies = true + } if (it.getString("vormerken") == "1") { - resInfo = barcode + resInfo = "reserve\t$barcode" + hasReservableCopies = true } // reservations: only available for reserved copies, not for reservable copies // url: not for accessible online resources, only for lendable online copies @@ -237,6 +259,7 @@ open class SLUB : OkHttpBaseApi() { } copies = copiesList } + isReservable = hasReservableCopies // volumes volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { 0.until(length()).map { optJSONObject(it) }?.map { @@ -253,7 +276,95 @@ open class SLUB : OkHttpBaseApi() { } override fun reservation(item: DetailedItem, account: Account, useraction: Int, selection: String?): OpacApi.ReservationResult { - TODO("not implemented") + var action = useraction + var selected = selection ?: "" + // step 1: select copy to request/reserve if there are multiple requestable/reservable copies + // if there's just one requestable/reservable copy then go to step 2 + if (action == 0 && selection == null) { + val reservableCopies = item.copies.filter { it.resInfo != null } + when (reservableCopies.size) { + 0 -> return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.NO_COPY_RESERVABLE)) + 1 -> { + action = ACTION_COPY + selected = reservableCopies.first().resInfo + } + else -> { + val options = reservableCopies.map { copy -> + mapOf("key" to copy.resInfo, + "value" to "${copy.branch}: ${copy.status}") + } + return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, + stringProvider.getString(StringProvider.COPY)).apply { + actionIdentifier = ACTION_COPY + this.selection = options + } + } + } + } + // step 2: select pickup branch (reservations) or pickup point (stack requests) + // if there's just one pickup point then go to step 3 + if (action == ACTION_COPY) { + val pickupLocations: Map + if (selected.startsWith("reserve")) { + pickupLocations = mapOf( + "zell1" to "Zentralbibliothek", + "bebel1" to "ZwB Erziehungswissenschaften", + "berg1" to "ZwB Rechtswissenschaft", + "fied1" to "ZwB Medizin", + "tha1" to "ZwB Forstwissenschaft", + "zell9" to "Bereichsbibliothek Drepunct" + ) + } else { + val data = selected.split('\t') + if (data.size != 2) { + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.INTERNAL_ERROR)) + } + try { + val json = requestAccount(account, "pickup", mapOf("tx_slubaccount_account[barcode]" to data[1])) + pickupLocations = json.optJSONArray("PickupPoints")?.run { + 0.until(length()).map { optString(it) }.map { + it to pickupPoints.getOrElse(it){it} + } + }?.toMap() ?: emptyMap() + } catch (e: OpacApi.OpacErrorException) { + return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } + } + when (pickupLocations.size) { + 0 -> return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.INTERNAL_ERROR)) + 1 -> { + action = OpacApi.ReservationResult.ACTION_BRANCH + selected += "\t${pickupLocations.keys.first()}" + } + else -> return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = pickupLocations.map { + mapOf("key" to "$selected\t${it.key}", + "value" to it.value) + } + } + } + } + // step 3. make stack request or reservation + if (action == OpacApi.ReservationResult.ACTION_BRANCH) { + val data = selected.split('\t') + if (data.size == 3) { + val pickupParameter = if (data[0] == "stackRequest") "pickupPoint" else "PickupBranch" + return try { + val json = requestAccount(account, data[0], + mapOf("tx_slubaccount_account[barcode]" to data[1], + "tx_slubaccount_account[$pickupParameter]" to data[2])) + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, json.optString("message")) + } catch (e: OpacApi.OpacErrorException) { + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) + } + } + } + return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, + stringProvider.getString(StringProvider.INTERNAL_ERROR)) } override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java index 999c624f6..a97eba9ec 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java @@ -92,6 +92,7 @@ public interface StringProvider { String READINGROOM = "readingroom"; String REQUEST_PROGRESS = "request_progress"; String RESERVED_POS = "reserved_pos"; + String COPY = "copy"; /** * Returns the translated string identified by identifier diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 6afa6af6d..3313f20af 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -1,3 +1,24 @@ +/* + * Copyright (C) 2020 by Steffen Rehberg under the MIT license: + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs @@ -5,6 +26,8 @@ import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.i18n.StringProvider.* import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* +import okhttp3.FormBody +import okhttp3.RequestBody import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* import org.joda.time.LocalDate @@ -16,8 +39,28 @@ import org.junit.Test import org.junit.rules.ExpectedException import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.junit.runners.Suite +import org.mockito.AdditionalMatchers.or +import org.mockito.ArgumentMatcher import org.mockito.Matchers +import org.mockito.Matchers.argThat import org.mockito.Mockito +import org.mockito.Mockito.verify + +/** + * Tests for SLUB API + * + * @author Steffen Rehberg, Jan 2020 + */ +@RunWith(Suite::class) +@Suite.SuiteClasses( + SLUBAccountTest::class, + SLUBSearchTest::class, + SLUBAccountMockTest::class, + SLUBReservationMockTest::class, + SLUBAccountValidateMockTest::class +) +class SLUBAllTests private class TestStringProvider : StringProvider { override fun getString(identifier: String?): String { @@ -215,6 +258,7 @@ class SLUBSearchTest() : BaseHtmlTest() { branch = "Zentralbibliothek" status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" shelfmark = "19 4 01339 0 0024 1 01" + resInfo = "stackRequest\t10418078" } val copyLast = Copy().apply { barcode = "33364639" @@ -222,6 +266,7 @@ class SLUBSearchTest() : BaseHtmlTest() { branch = "Zentralbibliothek" status = "Bestellen zur Benutzung im Haus, kein Versand per Fernleihe, nur Kopienlieferung" shelfmark = "19 4 01339 1 1969 1 01" + resInfo = "stackRequest\t33364639" } val item = slub.parseResultById(json.getString("id"), json) @@ -322,13 +367,66 @@ class SLUBSearchTest() : BaseHtmlTest() { assertThat(item, sameBeanAs(expected).ignoring("details")) assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) } + + @Test + fun testParseResultByIdResinfo() { + val json = JSONObject(readResource("/slub/search/item-for-reserve&request.json")) + val expected = DetailedItem().apply { + addDetail(Detail("Medientyp", "Buch")) + addDetail(Detail("Titel", "Der Fürstenzug zu Dresden: Denkmal und Geschichte des Hauses Wettin")) + title = "Der Fürstenzug zu Dresden: Denkmal und Geschichte des Hauses Wettin" + addDetail(Detail("Beteiligte", "Blaschke, Karlheinz [Autor/In]; Beyer, Klaus G. [Ill.]")) + addDetail(Detail("Erschienen", "Leipzig Jena Berlin Urania-Verl. 1991 ")) + addDetail(Detail("ISBN", "3332003771; 9783332003772")) + addDetail(Detail("Sprache", "Deutsch")) + addDetail(Detail("Schlagwörter", "Walther, Wilhelm; Albertiner; Albertiner; Fries; Walther, Wilhelm")) + addDetail(Detail("Beschreibung", "Literaturverz. S. 222 - 224")) + id = "0-276023927" + copies = arrayListOf( + Copy().apply { + barcode = "10059731" + department = "Freihand" + branch = "Zentralbibliothek" + status = "Ausgeliehen, Vormerken möglich" + shelfmark = "LK 24099 B644" + returnDate = LocalDate(2020, 2, 5) + resInfo = "reserve\t10059731" + }, + + Copy().apply { + barcode = "30523028" + department = "Freihand" + branch = "ZwB Erziehungswissenschaften" + status = "Benutzung nur im Haus, Versand per Fernleihe möglich" + shelfmark = "NR 6400 B644 F9" + }, + Copy().apply { + barcode = "20065307" + department = "Magazin" + branch = "Zentralbibliothek" + status = "Ausleihbar, bitte bestellen" + shelfmark = "65.4.653.b" + resInfo = "stackRequest\t20065307" + } + ) + isReservable = true + } + + val item = slub.parseResultById(json.getString("id"), json) + + assertThat(item, sameBeanAs(expected).ignoring("details")) + assertThat(HashSet(expected.details), sameBeanAs(HashSet(item.details))) + } } @RunWith(Parameterized::class) -class SLUBAccountMockTest(private val response: String, - private val expectedException: Class?, - private val expectedExceptionMsg: String?) : BaseHtmlTest() { +class SLUBAccountMockTest(@Suppress("unused") private val name: String, + private val response: String, + private val expectedMessage: String?, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { private val slub = Mockito.spy(SLUB::class.java) + init { slub.init(Library().apply { data = JSONObject().apply { @@ -336,6 +434,7 @@ class SLUBAccountMockTest(private val response: String, } }, HttpClientFactory("test")) } + private val account = Account().apply { name = "x" password = "x" @@ -353,24 +452,268 @@ class SLUBAccountMockTest(private val response: String, thrown.expectMessage(expectedExceptionMsg) } - slub.requestAccount(account, "", null) + val actual = slub.requestAccount(account, "", null) + assertEquals(expectedMessage, actual.optString("message")) } companion object { @JvmStatic - @Parameterized.Parameters + @Parameterized.Parameters(name = "{0}") fun data() = listOf( // validate: status as string - arrayOf("{\"status\":\"1\",\"message\":\"credentials_are_valid\"}", null, null), - arrayOf("{\"message\":\"error_credentials_invalid\",\"arguments\":{\"controller\":\"API\",\"action\":\"validate\",\"username\":\"123456\"},\"status\":\"-1\"}", OpacApi.OpacErrorException::class.java, "error_credentials_invalid"), + arrayOf("String - OK", "{\"status\":\"1\",\"message\":\"credentials_are_valid\"}", "credentials_are_valid", null, null), + arrayOf("String - Error", "{\"message\":\"error_credentials_invalid\",\"arguments\":{\"controller\":\"API\",\"action\":\"validate\",\"username\":\"123456\"},\"status\":\"-1\"}", null, OpacApi.OpacErrorException::class.java, "error_credentials_invalid"), // POST not accepted, malformed request, e.g. invalid action - arrayOf(".", OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), + arrayOf("Malformed", ".", null, OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), // delete: status as int or string - arrayOf("{\"status\":1,\"message\":\"Reservation deleted\"}", null, null), - arrayOf("{\"status\":\"-1\",\"message\":\"Item not reserved\"}", OpacApi.OpacErrorException::class.java, "Item not reserved"), + arrayOf("Int/string - OK", "{\"status\":1,\"message\":\"Reservation deleted\"}", "Reservation deleted", null, null), + arrayOf("Int/string - Error", "{\"status\":\"-1\",\"message\":\"Item not reserved\"}", null, OpacApi.OpacErrorException::class.java, "Item not reserved"), // pickup: status as boolean - arrayOf("{\"status\":true,\"message\":\"n\\/a\"}", null, null), - arrayOf("{\"status\":false,\"message\":\"Ungültige Barcodenummer\"}", OpacApi.OpacErrorException::class.java, "Ungültige Barcodenummer") + arrayOf("Boolean - OK", "{\"status\":true,\"message\":\"n\\/a\"}", "n/a", null, null), + arrayOf("Boolean - Error", "{\"status\":false,\"message\":\"Ungültige Barcodenummer\"}", null, OpacApi.OpacErrorException::class.java, "Ungültige Barcodenummer") ) } -} \ No newline at end of file +} + +@RunWith(Parameterized::class) +class SLUBReservationMockTest(@Suppress("unused") private val name: String, + private val item: DetailedItem, + private val useraction: Int, + private val selection: String?, + private val responsePickup: String?, + private val responseReserveOrRequest: String?, + private val expectedResult: OpacApi.ReservationResult) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "123456" + password = "x" + } + + @Test + fun testReservation() { + if (responsePickup != null) { + Mockito.doReturn(responsePickup).`when`(slub).httpPost(Matchers.any(), + argThat(IsRequestBodyWithAction("pickup")), Matchers.any()) + } + if (responseReserveOrRequest != null) { + Mockito.doReturn(responseReserveOrRequest).`when`(slub).httpPost(Matchers.any(), + or(argThat(IsRequestBodyWithAction("stackRequest")), + argThat(IsRequestBodyWithAction("reserve"))), Matchers.any()) + } + + val result = slub.reservation(item, account, useraction, selection) + assertThat(result, sameBeanAs(expectedResult)) + } + + companion object { + // this item has already been tested in testParseResultByIdResinfo so we can rely on parseResultById here + private val json = JSONObject(BaseHtmlTest().readResource("/slub/search/item-for-reserve&request.json")) + private val itemRequestAndReserve = SLUB().parseResultById(json.getString("id"), json) + private val itemRequest = SLUB().parseResultById(json.getString("id"), json) + private val itemReserve = SLUB().parseResultById(json.getString("id"), json) + private val itemNone = SLUB().parseResultById(json.getString("id"), json) + + init { + itemRequest.apply { + copies = copies.filter { + it.resInfo?.startsWith("stackRequest") ?: false + } + } + itemReserve.apply { + copies = copies.filter { + it.resInfo?.startsWith("reserve") ?: false + } + } + itemNone.apply { copies = copies.filter { it.resInfo == null } } + } + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = listOf( + arrayOf("Single reservable copy (with multiple pickup branches)", + itemReserve, + 0, + null, + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "reserve\t10059731\tzell1", "value" to "Zentralbibliothek"), + mapOf("key" to "reserve\t10059731\tbebel1", "value" to "ZwB Erziehungswissenschaften"), + mapOf("key" to "reserve\t10059731\tberg1", "value" to "ZwB Rechtswissenschaft"), + mapOf("key" to "reserve\t10059731\tfied1", "value" to "ZwB Medizin"), + mapOf("key" to "reserve\t10059731\ttha1", "value" to "ZwB Forstwissenschaft"), + mapOf("key" to "reserve\t10059731\tzell9", "value" to "Bereichsbibliothek Drepunct") + ) + } + ), + arrayOf("Make reservation (for selected pickup branch)", + itemReserve, + OpacApi.ReservationResult.ACTION_BRANCH, + "reserve\t10059731\tbebel1", + null, + "{\"status\":1,\"message\":\"Ihre Vormerkung wurde vorgenommen|Diese Vormerkung läuft ab am 23 Apr 2020|Position in der Vormerkerliste 1\",\"arguments\":{\"controller\":\"API\",\"action\":\"reserve\",\"barcode\":\"10059731\",\"username\":\"123456\",\"PickupBranch\":\"zell1\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Ihre Vormerkung wurde vorgenommen|Diese Vormerkung läuft ab am 23 Apr 2020|Position in der Vormerkerliste 1") + ), + arrayOf("Single requestable copy with single pickup point", + itemRequest, + 0, + null, + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") + ), + arrayOf("Single requestable copy with multiple pickup points", + itemRequest, + 0, + null, + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\",\"a13\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "stackRequest\t20065307\ta01", "value" to "Zentralbibliothek Ebene 0 SB-Regal"), + mapOf("key" to "stackRequest\t20065307\ta13", "value" to "Zentralbibliothek, Servicetheke") + ) + } + ), + arrayOf("Single requestable copy with multiple pickup points including unknown ones", + itemRequest, + 0, + null, + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\",\"xxx\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "stackRequest\t20065307\ta01", "value" to "Zentralbibliothek Ebene 0 SB-Regal"), + mapOf("key" to "stackRequest\t20065307\txxx", "value" to "xxx") + ) + } + ), + arrayOf("Make stack request for selected pickup point", + itemRequest, + OpacApi.ReservationResult.ACTION_BRANCH, + "stackRequest\t20065307\ta01", + null, + "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") + ), + arrayOf("Multiple requestable or reservable copies", + itemRequestAndReserve, + 0, + null, + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, "copy").apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_USER + 1 + this.selection = listOf( + mapOf("key" to "reserve\t10059731", "value" to "Zentralbibliothek: Ausgeliehen, Vormerken möglich"), + mapOf("key" to "stackRequest\t20065307", "value" to "Zentralbibliothek: Ausleihbar, bitte bestellen")) + } + ), + arrayOf("Selected reservable copy (with multiple pickup branches)", + itemRequestAndReserve, + OpacApi.ReservationResult.ACTION_USER + 1, // == ACTION_COPY + "reserve\t10059731", + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED).apply { + actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH + this.selection = listOf( + mapOf("key" to "reserve\t10059731\tzell1", "value" to "Zentralbibliothek"), + mapOf("key" to "reserve\t10059731\tbebel1", "value" to "ZwB Erziehungswissenschaften"), + mapOf("key" to "reserve\t10059731\tberg1", "value" to "ZwB Rechtswissenschaft"), + mapOf("key" to "reserve\t10059731\tfied1", "value" to "ZwB Medizin"), + mapOf("key" to "reserve\t10059731\ttha1", "value" to "ZwB Forstwissenschaft"), + mapOf("key" to "reserve\t10059731\tzell9", "value" to "Bereichsbibliothek Drepunct") + ) + } + ), + arrayOf("Selected requestable copy with single pickup point", + itemRequestAndReserve, + OpacApi.ReservationResult.ACTION_USER + 1, // == ACTION_COPY + "stackRequest\t20065307", + "{\"status\":true,\"message\":\"n\\/a\",\"PickupPoints\":[\"a01\"],\"arguments\":{\"controller\":\"API\",\"action\":\"pickup\",\"barcode\":\"20065307\",\"username\":\"123456\"}}", + "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") + ), + // "selected requestable copy wiht multiple pickup ponits" doesn't need to be tested as it's the same process as "Selected reservable copy (with multiple pickup branches)" + arrayOf("No requestable or reservable copies", + itemNone, + 0, + null, + null, + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, "no_copy_reservable") + ), + arrayOf("Error getting pickup points", + itemRequest, + 0, + null, + "", + null, + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, "unknown_error_account_with_description accountRequest didn't return JSON object: A JSONObject text must begin with '{' at character 0") + ) + ) + } +} + +class SLUBAccountValidateMockTest : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "test") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "x" + password = "x" + } + + @Test + fun testCheckAccountData() { + val response = "{\"status\":\"1\",\"message\":\"credentials_are_valid\"}" + Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) + + slub.checkAccountData(account) + verify(slub).httpPost(Matchers.any(), argThat(IsRequestBodyWithAction("validate")), Matchers.any()) + } +} + +class IsRequestBodyWithAction(private val action: String) : ArgumentMatcher() { + override fun matches(arg: Any ): Boolean { + val fb = arg as FormBody? + for(i in 0 until (fb?.size() ?: 0)){ + if (fb!!.value(i) == action) + return true + } + return false + } +} + +class IsRequestBodyWithActionTest { + val fb: FormBody = FormBody.Builder().add("name", "value").build() + @Test + fun `matcher matches`() = assertTrue(IsRequestBodyWithAction("value").matches(fb)) + + @Test + fun `matcher doesn't match`() = assertFalse(IsRequestBodyWithAction("").matches(fb)) + + @Test + fun `matcher doesn't match empty formbody`() = assertFalse(IsRequestBodyWithAction("").matches(FormBody.Builder().build())) +} diff --git a/opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json b/opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json new file mode 100644 index 000000000..9e35cf511 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/item-for-reserve&request.json @@ -0,0 +1,98 @@ +{ + "record": { + "format": "Buch", + "title": "Der Fürstenzug zu Dresden: Denkmal und Geschichte des Hauses Wettin", + "contributor": [ + "Blaschke, Karlheinz [Autor/In]", + "Beyer, Klaus G. [Ill.]" + ], + "publisher": [ + "Leipzig Jena Berlin Urania-Verl. 1991 " + ], + "ispartof": [], + "identifier": [ + "3332003771", + "9783332003772" + ], + "language": [ + "Deutsch" + ], + "subject": [ + "Walther, Wilhelm", + "Albertiner", + "Albertiner", + "Fries", + "Walther, Wilhelm" + ], + "description": [ + "Literaturverz. S. 222 - 224" + ], + "status": "", + "rvk": "" + }, + "id": "0-276023927", + "oa": 0, + "thumbnail": "", + "links": [], + "linksRelated": [], + "linksAccess": [], + "linksGeneral": [], + "references": [], + "copies": [ + { + "barcode": "10059731", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Freihand", + "shelfmark": "LK 24099 B644", + "mediatype": "B", + "3d": "LK 24099 B644", + "3d_link": "https://3d.slub-dresden.de/viewer?project_id=3&search_key=LK%2024099%20B644&language=de&search_context1=zell1&search_context2=FH1&exemplar_id=10059731", + "issue": "", + "colorcode": "3", + "statusphrase": "Ausgeliehen, Vormerken möglich", + "link": "", + "status": "N", + "duedate": "05.02.2020", + "vormerken": "1", + "bestellen": "0" + }, + { + "barcode": "30523028", + "location": "ZwB Erziehungswissenschaften", + "location_code": "bebel1", + "sublocation": "Freihand", + "shelfmark": "NR 6400 B644 F9", + "mediatype": "B", + "3d": "", + "3d_link": "", + "issue": "", + "colorcode": "1", + "statusphrase": "Benutzung nur im Haus, Versand per Fernleihe möglich", + "link": "", + "status": "P1", + "duedate": "", + "vormerken": "0", + "bestellen": "0" + }, + { + "barcode": "20065307", + "location": "Zentralbibliothek", + "location_code": "zell1", + "sublocation": "Magazin", + "shelfmark": "65.4.653.b", + "mediatype": "B", + "3d": "", + "3d_link": "", + "issue": "", + "colorcode": "2", + "statusphrase": "Ausleihbar, bitte bestellen", + "link": "", + "status": "NM", + "duedate": "", + "vormerken": "0", + "bestellen": "1" + } + ], + "parts": {} +} diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index 34c5962e6..ed20980b7 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -92,4 +92,5 @@ liegt seit %s zur Benutzung in Bibliothek bereit Magazinbestellung seit %s in Arbeit vorgemerkt, Pos. %s + Exemplar diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index ede468d2c..b1dbf6ee7 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -93,4 +93,5 @@ ready since %s for usage in library stack request in progress since %s reserved, pos. %s + Copy From d17be7e320def6e02907d825927df04648b273f2 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 29 Jan 2020 22:42:12 +0100 Subject: [PATCH 37/55] Fix code analysis issues and simplify isRenewable assignment --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 26 ++++++++----------- .../geeksfactory/opacclient/apis/SLUBTest.kt | 10 +++---- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index a3b9dc9a5..742d86fa3 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2020 by Steffen Rehberg under the MIT license: * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -48,7 +48,7 @@ import org.jsoup.parser.Parser */ open class SLUB : OkHttpBaseApi() { protected lateinit var baseurl: String - protected val ENCODING = "UTF-8" + private val ENCODING = "UTF-8" protected lateinit var query: List private val mediaTypes = mapOf( @@ -200,13 +200,11 @@ open class SLUB : OkHttpBaseApi() { this.id = id val record = json.optJSONObject("record") for (key in record.keys()) { - val v = record.get(key as String) - var value = when (v) { + var value = when (val v = record.get(key as String)) { is String -> v is Int -> v.toString() is JSONArray -> 0.until(v.length()).map { - val arrayItem = v.get(it) - when (arrayItem) { + when (val arrayItem = v.get(it)) { is String -> arrayItem is JSONObject -> arrayItem.optString("title").also { // if item is part of multiple collections, collectionsId holds the last one @@ -219,7 +217,7 @@ open class SLUB : OkHttpBaseApi() { } if (value.isNotEmpty()) { value = Parser.unescapeEntities(value, false) - if (key.equals("title")) { + if (key == "title") { title = value } addDetail(Detail(fieldCaptions[key], value)) @@ -231,7 +229,7 @@ open class SLUB : OkHttpBaseApi() { } } // links and references - for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ + for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ val linkArray = json.optJSONArray(link) linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map{ // assuming that only on of material, note or hostlabel is set @@ -248,7 +246,7 @@ open class SLUB : OkHttpBaseApi() { // copies val cps = json.opt("copies") if (cps is JSONArray) { - getCopies(cps, dateFormat)?.let { copies = it } + getCopies(cps, dateFormat).let { copies = it } } else { // multiple arrays val copiesList = mutableListOf() for (key in (cps as JSONObject).keys()) { @@ -262,7 +260,7 @@ open class SLUB : OkHttpBaseApi() { isReservable = hasReservableCopies // volumes volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { - 0.until(length()).map { optJSONObject(it) }?.map { + 0.until(length()).map { optJSONObject(it) }.map { Volume(it.optString("id"), "${it.optString("part")} ${Parser.unescapeEntities(it.optString("name"),false)}") } @@ -397,7 +395,7 @@ open class SLUB : OkHttpBaseApi() { internal fun parseAccountData(account: Account, json: JSONObject): AccountData { val fmt = DateTimeFormat.shortDate() fun getReservations(items: JSONObject?): MutableList { - val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") + val types = listOf("hold", "request_ready", "readingroom", "request_progress", "reserve") // "requests" is a copy of "request_ready" + "readingroom" + "request_progress" val reservationsList = mutableListOf() for (type in types) { @@ -470,11 +468,9 @@ open class SLUB : OkHttpBaseApi() { it.optInt("X_is_reserved") != 0 -> "vorgemerkt" else -> null } - isRenewable = if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items + if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items + isRenewable = true prolongData = barcode - true - } else { - false } } } ?: emptyList() diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 3313f20af..ebd737a07 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -85,8 +85,8 @@ private class TestStringProvider : StringProvider { } } -class SLUBAccountTest() : BaseHtmlTest() { - var slub = SLUB() +class SLUBAccountTest : BaseHtmlTest() { + private var slub = SLUB() init { slub.stringProvider = TestStringProvider() @@ -174,8 +174,8 @@ class SLUBAccountTest() : BaseHtmlTest() { } } -class SLUBSearchTest() : BaseHtmlTest() { - var slub = SLUB() +class SLUBSearchTest : BaseHtmlTest() { + private var slub = SLUB() @Test fun testParseEmptySearchResults() { @@ -279,7 +279,7 @@ class SLUBSearchTest() : BaseHtmlTest() { @Test fun testParseResultByIdMultipleParts() { val json = JSONObject(readResource("/slub/search/item-multiple_parts_item.json")) - val volumes = listOf( + val volumes = listOf( Volume("0-1453040935", "[3]: Principles of digital image processing"), Volume("0-1347927328", "[2]: Principles of digital image processing"), Volume("0-1347930884", "[1]: Principles of digital image processing") From 0d52c8b5bb77bc9c563bb1b92ae5128a08c39701 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 1 Feb 2020 22:45:16 +0100 Subject: [PATCH 38/55] Fix error with no_criteria_input introduced in 118089a4 - and add tests for search - tests for parseSearchFields - add dropdown search field for access (physical/digital) and fix url build method the prevent issues with space in url query name part --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 28 +- .../src/main/resources/meanings/slub.json | 3 + .../geeksfactory/opacclient/apis/SLUBTest.kt | 102 +++- .../slub/SLUB Dresden - Katalog.html | 439 ++++++++++++++++++ 4 files changed, 564 insertions(+), 8 deletions(-) create mode 100644 opacclient/libopac/src/main/resources/meanings/slub.json create mode 100644 opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 742d86fa3..816a5ed0a 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -24,6 +24,7 @@ package de.geeksfactory.opacclient.apis import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* +import de.geeksfactory.opacclient.searchfields.DropdownSearchField import de.geeksfactory.opacclient.searchfields.SearchField import de.geeksfactory.opacclient.searchfields.SearchQuery import de.geeksfactory.opacclient.searchfields.TextSearchField @@ -109,19 +110,24 @@ open class SLUB : OkHttpBaseApi() { } override fun searchGetPage(page: Int): SearchRequestResult { - if (query.size <= 4) { - throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) - } val queryUrlB = HttpUrl.get("$baseurl/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app") .newBuilder() - .addQueryParameter("tx_find_find[page]", page.toString()) + .addEncodedQueryParameter("tx_find_find[page]", page.toString()) for (sq in query) { if (sq.value.isNotEmpty()) { - queryUrlB.addQueryParameter("tx_find_find[q][${sq.key}]", sq.value) + if(sq.searchField is DropdownSearchField){ // access_facet + queryUrlB.addEncodedQueryParameter("tx_find_find[facet][access_facet][${sq.value}]","1") + } else{ + queryUrlB.addEncodedQueryParameter("tx_find_find[q][${sq.key}]", sq.value) + } } } + val queryUrl = queryUrlB.build() + if (queryUrl.querySize() <= 4) { + throw OpacApi.OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)) + } try { - return parseSearchResults(JSONObject(httpGet(queryUrlB.build().toString(), ENCODING))) + return parseSearchResults(JSONObject(httpGet(queryUrl.toString(), ENCODING))) } catch (e: JSONException) { throw OpacApi.OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_WITH_DESCRIPTION, @@ -527,7 +533,15 @@ open class SLUB : OkHttpBaseApi() { id = it["name"] displayName = it.text } - } + } + listOf(DropdownSearchField().apply { + id = "access_facet" + displayName = "Zugang" + dropdownValues = listOf( + DropdownSearchField.Option("", ""), + DropdownSearchField.Option("Local Holdings", "physisch"), + DropdownSearchField.Option("Electronic Resources", "digital") + ) + }) } override fun setLanguage(language: String?) { diff --git a/opacclient/libopac/src/main/resources/meanings/slub.json b/opacclient/libopac/src/main/resources/meanings/slub.json new file mode 100644 index 000000000..ce0675854 --- /dev/null +++ b/opacclient/libopac/src/main/resources/meanings/slub.json @@ -0,0 +1,3 @@ +{ + "Zugang": "DIGITAL" +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index ebd737a07..ef80033b1 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -26,6 +26,9 @@ import de.geeksfactory.opacclient.i18n.StringProvider import de.geeksfactory.opacclient.i18n.StringProvider.* import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* +import de.geeksfactory.opacclient.searchfields.DropdownSearchField +import de.geeksfactory.opacclient.searchfields.SearchQuery +import de.geeksfactory.opacclient.searchfields.TextSearchField import okhttp3.FormBody import okhttp3.RequestBody import org.hamcrest.MatcherAssert.assertThat @@ -58,7 +61,9 @@ import org.mockito.Mockito.verify SLUBSearchTest::class, SLUBAccountMockTest::class, SLUBReservationMockTest::class, - SLUBAccountValidateMockTest::class + SLUBAccountValidateMockTest::class, + SLUBSearchMockTest::class, + SLUBSearchFieldsMockTest::class ) class SLUBAllTests @@ -695,6 +700,101 @@ class SLUBAccountValidateMockTest : BaseHtmlTest() { } } +@RunWith(Parameterized::class) +class SLUBSearchMockTest(@Suppress("unused") private val name: String, + private val query: List, + private val expectedQueryUrl: String?, + private val response: String?, + private val expectedResultCount: Int?, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + } + }, HttpClientFactory("test")) + } + + @JvmField + @Rule + var thrown: ExpectedException = ExpectedException.none() + + @Test + fun testSearch() { + Mockito.doReturn(response).`when`(slub).httpGet(Matchers.any(), Matchers.any()) + if (expectedException != null) { + thrown.expect(expectedException) + thrown.expectMessage(expectedExceptionMsg) + } + + val actual = slub.search(query) + assertEquals(expectedResultCount, actual.total_result_count) + verify(slub).httpGet(expectedQueryUrl,"UTF-8") + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = listOf( + arrayOf("Empty query", + emptyList(), + null, + null, + 0, + OpacApi.OpacErrorException::class.java, + "no_criteria_input" + ), + arrayOf("Drop-down and text field", + listOf( + SearchQuery(TextSearchField().apply { + id = "title" + displayName = "Titel" + }, "Kotlin - Das umfassende Praxis-Handbuch"), + SearchQuery(DropdownSearchField().apply { + id = "access_facet" + displayName = "Zugang" + dropdownValues = listOf( + DropdownSearchField.Option("Local+Holdings", "physisch"), + DropdownSearchField.Option("Electronic+Resources", "digital") + ) + }, "Electronic+Resources") + ), + "https://test.de/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app&tx_find_find[page]=1&tx_find_find[q][title]=Kotlin - Das umfassende Praxis-Handbuch&tx_find_find[facet][access_facet][Electronic+Resources]=1".replace(" ", "%20"), // correct for addEncodedQueryParameter + "{\"numFound\":1,\"start\" : 0,\"docs\" : [{\"id\":\"0-1688062912\",\"format\":[\"Book, E-Book\"],\"title\":\"Kotlin - Das umfassende Praxis-Handbuch Szwillus, Karl.\",\"author\":[\"Szwillus, Karl\"],\"creationDate\":\"2019\",\"imprint\":[\"[Erscheinungsort nicht ermittelbar]: mitp Verlag, 2019\"]}]}", + 1, + null, + null + ) + ) + } +} + + +class SLUBSearchFieldsMockTest : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "test") + } + }, HttpClientFactory("test")) + } + + @Test + fun testParseSearchFields() { + val html = readResource("/slub/SLUB Dresden - Katalog.html") + Mockito.doReturn(html).`when`(slub).httpGet(Matchers.any(), Matchers.any()) + + val searchFields = slub.parseSearchFields() + assertEquals(10, searchFields.filterIsInstance(TextSearchField::class.java).size) + assertEquals(1, searchFields.filterIsInstance(DropdownSearchField::class.java).size) + } +} + class IsRequestBodyWithAction(private val action: String) : ArgumentMatcher() { override fun matches(arg: Any ): Boolean { val fb = arg as FormBody? diff --git a/opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html b/opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html new file mode 100644 index 000000000..eaa8470c0 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html @@ -0,0 +1,439 @@ + + + + + + + + + +SLUB Dresden - Katalog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+
+
+
+
DE
+
+ +
+
+
+
+ +
+ + + +
+ + + + + + + + + + +
+
+
+ + Willkommen im Werkstattbereich. + +


+

Hier entsteht unser neuer SLUB-Katalog.

+


+

Testen Sie die Recherchefunktionen und den Informationszugriff. Wir freuen uns über Ihre Eindrücke und Erfahrungen. Jedes Feedback, das zur weiteren Entwicklung unseres zentralen Werkzeuges beiträgt, hilft uns.

+


+

Ausführliche Informationen zu diesem Projekt finden Sie auf unserer Projektseite.

+
+
+
+ + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + \ No newline at end of file From 15195288e549e2e93bc9e57da94e9c11f301a3b9 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Fri, 31 Jan 2020 15:37:40 +0100 Subject: [PATCH 39/55] Refactor mapping of json array to map for pickup points to simplify code --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 816a5ed0a..19a65a5c2 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -328,10 +328,10 @@ open class SLUB : OkHttpBaseApi() { try { val json = requestAccount(account, "pickup", mapOf("tx_slubaccount_account[barcode]" to data[1])) pickupLocations = json.optJSONArray("PickupPoints")?.run { - 0.until(length()).map { optString(it) }.map { - it to pickupPoints.getOrElse(it){it} + 0.until(length()).map { optString(it) }.associateWith { + pickupPoints.getOrElse(it) { it } } - }?.toMap() ?: emptyMap() + } ?: emptyMap() } catch (e: OpacApi.OpacErrorException) { return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) } From 5c5fd7758613f934cb96b0c3dc97e1af00e3204c Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 21 Mar 2020 18:58:56 +0100 Subject: [PATCH 40/55] Implement renewal for inter-library loan items and tests for it --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 23 ++++- .../geeksfactory/opacclient/apis/SLUBTest.kt | 92 ++++++++++++++++++- .../resources/slub/account/ill-renew.html | 36 ++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 opacclient/libopac/src/test/resources/slub/account/ill-renew.html diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 19a65a5c2..7d0d15c34 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -49,6 +49,7 @@ import org.jsoup.parser.Parser */ open class SLUB : OkHttpBaseApi() { protected lateinit var baseurl: String + protected lateinit var illRenewUrl: String private val ENCODING = "UTF-8" protected lateinit var query: List @@ -102,6 +103,7 @@ open class SLUB : OkHttpBaseApi() { override fun init(library: Library, factory: HttpClientFactory) { super.init(library, factory) baseurl = library.data.getString("baseurl") + illRenewUrl = library.data.getString("illrenewurl") } override fun search(query: List): SearchRequestResult { @@ -372,9 +374,24 @@ open class SLUB : OkHttpBaseApi() { } override fun prolong(media: String, account: Account, useraction: Int, selection: String?): OpacApi.ProlongResult { + val data = media.split('\t') + if (data.size != 2) { + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.ERROR, "internal error") + } return try { - requestAccount(account, "renew", mapOf("tx_slubaccount_account[renewals][0]" to media)) - OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK) + if (data[0] == "FL") { + val formBody = FormBody.Builder() + .add("bc", data[1]) + .add("uid", account.name) + .add("clang", "DE") + .add("action", "send") + .build() + val result = httpPost(illRenewUrl, formBody, ENCODING).html + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK, result.select("p:last-of-type").text()) + } else { + requestAccount(account, "renew", mapOf("tx_slubaccount_account[renewals][0]" to data[1])) + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK) + } } catch (e: Exception) { OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.ERROR, e.message) } @@ -476,7 +493,7 @@ open class SLUB : OkHttpBaseApi() { } if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items isRenewable = true - prolongData = barcode + prolongData = "$format\t$barcode" } } } ?: emptyList() diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index ef80033b1..33f57bdf3 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -47,6 +47,7 @@ import org.mockito.AdditionalMatchers.or import org.mockito.ArgumentMatcher import org.mockito.Matchers import org.mockito.Matchers.argThat +import org.mockito.Matchers.eq import org.mockito.Mockito import org.mockito.Mockito.verify @@ -63,7 +64,8 @@ import org.mockito.Mockito.verify SLUBReservationMockTest::class, SLUBAccountValidateMockTest::class, SLUBSearchMockTest::class, - SLUBSearchFieldsMockTest::class + SLUBSearchFieldsMockTest::class, + SLUBProlongMockTest::class ) class SLUBAllTests @@ -121,7 +123,7 @@ class SLUBAccountTest : BaseHtmlTest() { //id = "31626878" barcode = "31626878" isRenewable = true - prolongData = barcode + prolongData = "$format\t$barcode" } val reserveditem1 = ReservedItem().apply { // reserve @@ -436,6 +438,7 @@ class SLUBAccountMockTest(@Suppress("unused") private val name: String, slub.init(Library().apply { data = JSONObject().apply { put("baseurl", "https://test.de") + put("illrenewurl", "https://test-renew.de") } }, HttpClientFactory("test")) } @@ -494,6 +497,7 @@ class SLUBReservationMockTest(@Suppress("unused") private val name: String, slub.init(Library().apply { data = JSONObject().apply { put("baseurl", "https://test.de") + put("illrenewurl", "https://test-renew.de") } }, HttpClientFactory("test")) } @@ -681,6 +685,7 @@ class SLUBAccountValidateMockTest : BaseHtmlTest() { slub.init(Library().apply { data = JSONObject().apply { put("baseurl", "test") + put("illrenewurl", "https://test-renew.de") } }, HttpClientFactory("test")) } @@ -714,6 +719,7 @@ class SLUBSearchMockTest(@Suppress("unused") private val name: String, slub.init(Library().apply { data = JSONObject().apply { put("baseurl", "https://test.de") + put("illrenewurl", "https://test-renew.de") } }, HttpClientFactory("test")) } @@ -772,6 +778,87 @@ class SLUBSearchMockTest(@Suppress("unused") private val name: String, } } +@RunWith(Parameterized::class) +class SLUBProlongMockTest(@Suppress("unused") private val name: String, + private val media: String, + private val expectedQueryUrl: String?, + private val expectedRequestBody: RequestBody, + private val response: String?, + private val expectedResult: OpacApi.MultiStepResult, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + put("illrenewurl", "https://test-renew.de") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "123456" + password = "x" + } + + @JvmField + @Rule + var thrown: ExpectedException = ExpectedException.none() + + @Test + fun testProlong() { + Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) + if (expectedException != null) { + thrown.expect(expectedException) + thrown.expectMessage(expectedExceptionMsg) + } + + val actualResult = slub.prolong(media, account, 0, null) + assertThat(actualResult, sameBeanAs(expectedResult)) + verify(slub).httpPost(eq(expectedQueryUrl), argThat(sameBeanAs(expectedRequestBody)), eq("UTF-8")) + } + + companion object { + private val illRenewResponse = BaseHtmlTest().readResource("/slub/account/ill-renew.html") + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = listOf( + arrayOf("Regular item", + "B\t20148242", + "https://test.de/mein-konto/", + FormBody.Builder() + .add("type", "1") + .add("tx_slubaccount_account[controller]", "API") + .add("tx_slubaccount_account[action]", "renew") + .add("tx_slubaccount_account[username]", "123456") + .add("tx_slubaccount_account[password]", "x") + .add("tx_slubaccount_account[renewals][0]", "20148242") + .build(), + "{\"status\":\"1\",\"arguments\":{\"controller\":\"API\",\"action\":\"renew\",\"username\":\"123456\",\"renewals\":[\"20148242\"]}}", + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK), + null, + null + ), + arrayOf("Interlibrary loan item", + "FL\t12022302N", + "https://test-renew.de", + FormBody.Builder() + .add("bc", "12022302N") + .add("uid", "123456") + .add("clang", "DE") + .add("action", "send") + .build(), + illRenewResponse, + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK, "Ihr Verlängerungswunsch wurde gesendet."), + null, + null + ) + ) + } +} class SLUBSearchFieldsMockTest : BaseHtmlTest() { private val slub = Mockito.spy(SLUB::class.java) @@ -780,6 +867,7 @@ class SLUBSearchFieldsMockTest : BaseHtmlTest() { slub.init(Library().apply { data = JSONObject().apply { put("baseurl", "test") + put("illrenewurl", "https://test-renew.de") } }, HttpClientFactory("test")) } diff --git a/opacclient/libopac/src/test/resources/slub/account/ill-renew.html b/opacclient/libopac/src/test/resources/slub/account/ill-renew.html new file mode 100644 index 000000000..ffd552163 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/ill-renew.html @@ -0,0 +1,36 @@ + + + Verlängerungswunsch für Fernleihmedium + + + + + + +
+

Verlängerungswunsch für Fernleihmedium

Medieninformation:

+ + + + + + +
Titel:Kotlin
Autor/Hrsg.:Szwillus, Karl;
Fälligkeit:07/02/2020 + +
RSN:16716746
ID:145073
Barcode:12022302N
+

Ihr Verlängerungswunsch wurde gesendet. \ No newline at end of file From 47ecd6af11a0bd819514144716b2606fbed6074d Mon Sep 17 00:00:00 2001 From: StefRe Date: Sat, 21 Mar 2020 19:02:58 +0100 Subject: [PATCH 41/55] Fix various code formatting issues --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 32 +++++++++---------- .../geeksfactory/opacclient/apis/SLUBTest.kt | 27 ++++++++-------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 7d0d15c34..0603e79b9 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -117,9 +117,9 @@ open class SLUB : OkHttpBaseApi() { .addEncodedQueryParameter("tx_find_find[page]", page.toString()) for (sq in query) { if (sq.value.isNotEmpty()) { - if(sq.searchField is DropdownSearchField){ // access_facet - queryUrlB.addEncodedQueryParameter("tx_find_find[facet][access_facet][${sq.value}]","1") - } else{ + if (sq.searchField is DropdownSearchField) { // access_facet + queryUrlB.addEncodedQueryParameter("tx_find_find[facet][access_facet][${sq.value}]", "1") + } else { queryUrlB.addEncodedQueryParameter("tx_find_find[q][${sq.key}]", sq.value) } } @@ -137,7 +137,7 @@ open class SLUB : OkHttpBaseApi() { } } - internal fun parseSearchResults(json: JSONObject): SearchRequestResult{ + internal fun parseSearchResults(json: JSONObject): SearchRequestResult { val searchresults = json.optJSONArray("docs")?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } ?.map { SearchResult().apply { @@ -163,7 +163,7 @@ open class SLUB : OkHttpBaseApi() { override fun getResultById(id: String, homebranch: String?): DetailedItem { val json: JSONObject try { - json = JSONObject(httpGet( + json = JSONObject(httpGet( "$baseurl/id/$id/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app", ENCODING)) } catch (e: JSONException) { @@ -174,7 +174,7 @@ open class SLUB : OkHttpBaseApi() { return parseResultById(id, json) } - internal fun parseResultById(id:String, json: JSONObject): DetailedItem { + internal fun parseResultById(id: String, json: JSONObject): DetailedItem { val dateFormat = DateTimeFormat.forPattern("dd.MM.yyyy") var hasReservableCopies = false fun getCopies(copiesArray: JSONArray, df: DateTimeFormatter): List = @@ -237,19 +237,19 @@ open class SLUB : OkHttpBaseApi() { } } // links and references - for (link in listOf("linksRelated", "linksAccess", "linksGeneral")){ + for (link in listOf("linksRelated", "linksAccess", "linksGeneral")) { val linkArray = json.optJSONArray(link) - linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map{ + linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map { // assuming that only on of material, note or hostlabel is set val key = with(it.optString("material") + it.optString("note") + it.optString("hostLabel")) { if (isEmpty()) fieldCaptions[link] else this } - addDetail(Detail( key, it.optString("uri"))) + addDetail(Detail(key, it.optString("uri"))) } } - json.optJSONArray("references").run { 0.until(length()).map { optJSONObject(it) } }.map{ + json.optJSONArray("references").run { 0.until(length()).map { optJSONObject(it) } }.map { // TODO: usually links to old SLUB catalogue, does it make sense to add the link? - addDetail(Detail( it.optString("text"), "${it.optString("name")} (${it.optString("target")})")) + addDetail(Detail(it.optString("text"), "${it.optString("name")} (${it.optString("target")})")) } // copies val cps = json.opt("copies") @@ -270,7 +270,7 @@ open class SLUB : OkHttpBaseApi() { volumes = json.optJSONObject("parts")?.optJSONArray("records")?.run { 0.until(length()).map { optJSONObject(it) }.map { Volume(it.optString("id"), - "${it.optString("part")} ${Parser.unescapeEntities(it.optString("name"),false)}") + "${it.optString("part")} ${Parser.unescapeEntities(it.optString("name"), false)}") } } ?: emptyList() } @@ -298,7 +298,7 @@ open class SLUB : OkHttpBaseApi() { else -> { val options = reservableCopies.map { copy -> mapOf("key" to copy.resInfo, - "value" to "${copy.branch}: ${copy.status}") + "value" to "${copy.branch}: ${copy.status}") } return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, stringProvider.getString(StringProvider.COPY)).apply { @@ -349,7 +349,7 @@ open class SLUB : OkHttpBaseApi() { actionIdentifier = OpacApi.ReservationResult.ACTION_BRANCH this.selection = pickupLocations.map { mapOf("key" to "$selected\t${it.key}", - "value" to it.value) + "value" to it.value) } } } @@ -362,7 +362,7 @@ open class SLUB : OkHttpBaseApi() { return try { val json = requestAccount(account, data[0], mapOf("tx_slubaccount_account[barcode]" to data[1], - "tx_slubaccount_account[$pickupParameter]" to data[2])) + "tx_slubaccount_account[$pickupParameter]" to data[2])) OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, json.optString("message")) } catch (e: OpacApi.OpacErrorException) { OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.ERROR, e.message) @@ -430,7 +430,7 @@ open class SLUB : OkHttpBaseApi() { author = it.optJSONArray("X_author")?.optString(0) //id = it.optString("label") // TODO: get details from here via /bc --> redirects to /id, from there get the proper id format = it.optString("X_medientyp") - status = when(type){ // TODO: maybe we need time (LocalDateTime) too make an educated guess on actual ready date for stack requests + status = when (type) { // TODO: maybe we need time (LocalDateTime) too make an educated guess on actual ready date for stack requests "hold" -> stringProvider.getFormattedString(StringProvider.HOLD, fmt.print(LocalDate(it.optString("X_date_reserved").substring(0, 10)))) "request_ready" -> stringProvider.getFormattedString(StringProvider.REQUEST_READY, diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 33f57bdf3..bcef6af6b 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -344,8 +344,8 @@ class SLUBSearchTest : BaseHtmlTest() { addDetail(Detail("Schlagwörter", "Java; JUnit")) addDetail(Detail("Beschreibung", "Literaturverz. S. 351")) id = "0-727434322" - addDetail(Detail("Inhaltsverzeichnis","http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf")) - addDetail(Detail("Inhaltstext","http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm")) + addDetail(Detail("Inhaltsverzeichnis", "http://www.gbv.de/dms/tib-ub-hannover/727434322.pdf")) + addDetail(Detail("Inhaltstext", "http://deposit.d-nb.de/cgi-bin/dokserv?id=4155321&prov=M&dok_var=1&dok_ext=htm")) addDetail(Detail("Zugang zur Ressource (via ProQuest Ebook Central)", "http://wwwdb.dbod.de/login?url=http://slub.eblib.com/patron/FullRecord.aspx?p=1575685")) addDetail(Detail("Online-Ausgabe", "Tamm, Michael: JUnit-Profiwissen (SLUB)")) } @@ -707,12 +707,12 @@ class SLUBAccountValidateMockTest : BaseHtmlTest() { @RunWith(Parameterized::class) class SLUBSearchMockTest(@Suppress("unused") private val name: String, - private val query: List, - private val expectedQueryUrl: String?, - private val response: String?, - private val expectedResultCount: Int?, - private val expectedException: Class?, - private val expectedExceptionMsg: String?) : BaseHtmlTest() { + private val query: List, + private val expectedQueryUrl: String?, + private val response: String?, + private val expectedResultCount: Int?, + private val expectedException: Class?, + private val expectedExceptionMsg: String?) : BaseHtmlTest() { private val slub = Mockito.spy(SLUB::class.java) init { @@ -738,7 +738,7 @@ class SLUBSearchMockTest(@Suppress("unused") private val name: String, val actual = slub.search(query) assertEquals(expectedResultCount, actual.total_result_count) - verify(slub).httpGet(expectedQueryUrl,"UTF-8") + verify(slub).httpGet(expectedQueryUrl, "UTF-8") } companion object { @@ -763,8 +763,8 @@ class SLUBSearchMockTest(@Suppress("unused") private val name: String, id = "access_facet" displayName = "Zugang" dropdownValues = listOf( - DropdownSearchField.Option("Local+Holdings", "physisch"), - DropdownSearchField.Option("Electronic+Resources", "digital") + DropdownSearchField.Option("Local+Holdings", "physisch"), + DropdownSearchField.Option("Electronic+Resources", "digital") ) }, "Electronic+Resources") ), @@ -884,9 +884,9 @@ class SLUBSearchFieldsMockTest : BaseHtmlTest() { } class IsRequestBodyWithAction(private val action: String) : ArgumentMatcher() { - override fun matches(arg: Any ): Boolean { + override fun matches(arg: Any): Boolean { val fb = arg as FormBody? - for(i in 0 until (fb?.size() ?: 0)){ + for (i in 0 until (fb?.size() ?: 0)) { if (fb!!.value(i) == action) return true } @@ -896,6 +896,7 @@ class IsRequestBodyWithAction(private val action: String) : ArgumentMatcher Date: Sat, 21 Mar 2020 22:54:43 +0100 Subject: [PATCH 42/55] Fix parseSearchFields according to new catalogue homepage 'baseurl' actually gets redirected to https://www.slub-dresden.de/ which then gets redirected to https://www.slub-dresden.de/startseite/. This works as the default 'followRedirects' setting in OkHttpClient Builder() is true. --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 4 +- .../geeksfactory/opacclient/apis/SLUBTest.kt | 2 +- .../slub/SLUB Dresden - Katalog.html | 439 ---- .../slub/SLUB Dresden: Startseite.html | 1874 +++++++++++++++++ 4 files changed, 1877 insertions(+), 442 deletions(-) delete mode 100644 opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html create mode 100644 opacclient/libopac/src/test/resources/slub/SLUB Dresden: Startseite.html diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 0603e79b9..c17b0e277 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -545,9 +545,9 @@ open class SLUB : OkHttpBaseApi() { override fun parseSearchFields(): List { val doc = httpGet(baseurl, ENCODING).html - return doc.select("ul#search-in-field-options li").map { + return doc.select("ul#search-in-field-recreated-list li").map { TextSearchField().apply { - id = it["name"] + id = it["id"] displayName = it.text } } + listOf(DropdownSearchField().apply { diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index bcef6af6b..f9846e05e 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -874,7 +874,7 @@ class SLUBSearchFieldsMockTest : BaseHtmlTest() { @Test fun testParseSearchFields() { - val html = readResource("/slub/SLUB Dresden - Katalog.html") + val html = readResource("/slub/SLUB Dresden: Startseite.html") Mockito.doReturn(html).`when`(slub).httpGet(Matchers.any(), Matchers.any()) val searchFields = slub.parseSearchFields() diff --git a/opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html b/opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html deleted file mode 100644 index eaa8470c0..000000000 --- a/opacclient/libopac/src/test/resources/slub/SLUB Dresden - Katalog.html +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - - - - -SLUB Dresden - Katalog - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - -
- - - -
-
-
-
-
DE
-
- -
-
-
-
- -
- - - -
- - - - - - - - - - -
-
-
- - Willkommen im Werkstattbereich. - -


-

Hier entsteht unser neuer SLUB-Katalog.

-


-

Testen Sie die Recherchefunktionen und den Informationszugriff. Wir freuen uns über Ihre Eindrücke und Erfahrungen. Jedes Feedback, das zur weiteren Entwicklung unseres zentralen Werkzeuges beiträgt, hilft uns.

-


-

Ausführliche Informationen zu diesem Projekt finden Sie auf unserer Projektseite.

-
-
-
- - - - -
- - - - - - - - - - - - - - - - -
- - - - - - - \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/SLUB Dresden: Startseite.html b/opacclient/libopac/src/test/resources/slub/SLUB Dresden: Startseite.html new file mode 100644 index 000000000..a951e85c0 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/SLUB Dresden: Startseite.html @@ -0,0 +1,1874 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +SLUB Dresden: Startseite + + + + + + + + +
+ + + +
+
+ +
+
+ + + + + +
+
+

Nur in Feld suchen:

+
    +
  • Alles
  • +
  • Person/Institution
  • +
  • Titel
  • +
  • Schlagwort
  • +
  • Barcode
  • +
  • ISBN/ISSN/ISMN
  • +
  • RVK-Notation
  • +
  • Signatur
  • +
  • Verlag/Ort
  • +
  • Serie/Reihe
  • +
+
+ +
+

Zuletzt gesuchte Begriffe:

+
+
+ +
+ + + + + + + + +
+ + + +
+
+
+ + +
+ + + + + + + + + +
+ + + + + + + +

Um einer weiteren Verbreitung des Coronavirus entgegenzuwirken, schließt die SLUB ab Samstag, den 14.3.2020, bis auf Weiteres alle Standorte für den Publikumsverkehr. Die digitalen Angebote stehen wie gewohnt zur Verfügung. Ausführliche Informationen unter www.slubdd.de/corona

+
+ + + + + + + + + +
+ + + +
+
+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +

Standorte

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +

Aktuelles im SLUBlog

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + + + + +

Digitale Services

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ + + +
+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +

SLUB Social Media

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Sie möchten nie wieder auf die neuesten Geschichten aus der SLUB verzichten? Auf Twitter, Facebook und Instagram erfahren Sie jederzeit und überall, was uns gerade bewegt. Folgen Sie uns, wo immer Sie mögen!

+
+ +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +

Sie interessieren sich für die gesamte SLUB-Markenfamilie? Dann schauen Sie doch auch auf diesen Kanälen einmal vorbei.

+

Saxorum | Musiconn | Deutsche Fotothek | arthistoricum | …

+
+ +
+ + + + + + +
+ + + +
+
+ +
+ + + + + + + + +
+ +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +

Powered by SLUB

+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Die Sächsische Landesbibliothek – Staats- und Universitätsbibliothek ist eine der leistungsfähigsten wissenschaftlichen Bibliotheken in Deutschland. Wir arbeiten täglich dafür, Ihnen, unseren Nutzerinnen und Nutzern, den bestmöglichen Service zu bieten. Zur SLUB-Markenfamilie gehören Rechercheportale, Fachinformationsdienste, Softwarelösungen und vieles mehr. Jüngster Zuwachs: das Landeskundeportal Saxorum.

+
+ + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 896f5f606d30dceb60ce16b4f31afd9772df8592 Mon Sep 17 00:00:00 2001 From: StefRe Date: Sun, 27 Sep 2020 00:38:49 +0200 Subject: [PATCH 43/55] Fix status message Provide status only if not renewable. Status gives the reason why the item is not renewable (SLUB changed its policy: items can be renewed more than 2 times now) --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 14 +- .../geeksfactory/opacclient/apis/SLUBTest.kt | 15 +- .../slub/account/account-status.json | 153 ++++++++++++++++++ .../test/resources/slub/account/account.json | 2 +- 4 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 opacclient/libopac/src/test/resources/slub/account/account-status.json diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index c17b0e277..57d30ebe4 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -486,14 +486,16 @@ open class SLUB : OkHttpBaseApi() { format = it.optString("X_medientyp") //id = it.optString("label") // TODO: get details from here via /bc --> redirects to /id, from there get the proper id barcode = it.optString("X_barcode") - status = when { - it.optInt("renewals") == 2 -> "2x verlängert" - it.optInt("X_is_reserved") != 0 -> "vorgemerkt" - else -> null - } if (it.optInt("X_is_renewable") == 1) { // TODO: X_is_flrenewable for ill items isRenewable = true prolongData = "$format\t$barcode" + } else { + isRenewable = false + status = when { + it.optInt("X_is_reserved") != 0 -> "vorgemerkt" // TODO: change to translatable string + it.optInt("renewals") > 0 -> "${it.optInt("renewals")}x verlängert" // TODO: change to translatable string + else -> null + } } } } ?: emptyList() @@ -539,7 +541,7 @@ open class SLUB : OkHttpBaseApi() { } override fun getSupportedLanguages(): Set? { - //TODO("not implemented") + //TODO("not implemented") return null } diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index f9846e05e..980757110 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -157,7 +157,6 @@ class SLUBAccountTest : BaseHtmlTest() { assertEquals(2, accountdata.lent.size) assertEquals(3, accountdata.reservations.size) assertThat(lentitem1, samePropertyValuesAs(accountdata.lent[0])) - assertEquals("vorgemerkt", accountdata.lent[1].status) assertThat(accountdata.reservations, hasItems(sameBeanAs(reserveditem1), sameBeanAs(reserveditem2), sameBeanAs(reserveditem3))) } @@ -179,6 +178,20 @@ class SLUBAccountTest : BaseHtmlTest() { assertEquals(1, accountdata.reservations.size) assertThat(reserveditem, samePropertyValuesAs(accountdata.reservations[0])) } + + @Test + fun testParseAccountDataStatus() { + val json = JSONObject(readResource("/slub/account/account-status.json")) + + val accountdata = slub.parseAccountData(Account(), json) + + assertEquals("9x verlängert", accountdata.lent[0].status) + assertEquals("vorgemerkt", accountdata.lent[1].status) + assertEquals(null, accountdata.lent[2].status) + assertEquals(false, accountdata.lent[0].isRenewable) + assertEquals(false, accountdata.lent[1].isRenewable) + assertEquals(true, accountdata.lent[2].isRenewable) + } } class SLUBSearchTest : BaseHtmlTest() { diff --git a/opacclient/libopac/src/test/resources/slub/account/account-status.json b/opacclient/libopac/src/test/resources/slub/account/account-status.json new file mode 100644 index 000000000..cf13a2e8b --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/account/account-status.json @@ -0,0 +1,153 @@ +{ + "status": "1", + "userID": "1234567", + "memberInfo": { + "name": " Max Mustermann", + "email": "max.mustermann@googlemail.com", + "address": "Musterstr. 1, 01069 Dresden", + "expires": "2020-03-31T00:00:00Z", + "status": "N", + "X_status_desc": "Normal", + "X_name": { + "givenname": "Max", + "surname": "Mustermann" + }, + "X_post_address": { + "street_1": "Musterstr. 1", + "plz": "01069", + "ort": "Dresden" + }, + "X_residental_address": [], + "X_alternative_address": [], + "X_date_of_joining": "2000-01-01T00:00:00Z", + "X_date_of_last_issue_int": "65143", + "X_date_of_last_issue_ext": "2019-05-10", + "X_gender": "M", + "X_branch": "zell1", + "X_branch_desc": "Zentralbibliothek", + "X_category": "NE", + "X_category_desc": "Normal Email", + "X_blacklists": [], + "X_notes": [], + "X_webopac_notes": [], + "type": "NE" + }, + "items":{ + "loan":[ + { + "status":3, + "about":"Nutzpflanzen der Tropen und Subtropen", + "label":"34751973", + "queue":"0", + "renewals":9, + "starttime":"2020-01-18T11:29:49Z", + "endtime":"2020-09-29T22:00:00Z", + "X_status_desc":"on loan", + "X_barcode":"34751973", + "X_author":[ + "Nowak, Bernd ¬[VerfasserIn]¬", + "Schulz, Bettina ¬[VerfasserIn]¬" + ], + "X_medientyp":"B", + "X_exstatus":"N", + "X_exstatus_desc":"Ausleihbar", + "X_shelfLocation":"ZE 30500 N946", + "X_internal_date_issued":"65396,44989", + "X_date_issued":"2020-01-18 12:29:49", + "X_internal_date_due":"65652.", + "X_date_due":"2020-09-30", + "X_days_to_due":"4", + "X_is_reserved":0, + "X_is_renewable":0, + "X_is_flrenewable":0, + "X_internal_date_renewed":"65624", + "X_uni_date_renewed":"2020-09-01T22:00:00Z", + "X_date_renewed":"2020-09-02", + "X_rsn":16669700 + }, + { + "status":3, + "about":"Nie wieder krank", + "label":"33494647", + "queue":"1", + "renewals":0, + "starttime":"2020-09-09T15:34:20Z", + "endtime":"2020-10-06T22:00:00Z", + "X_status_desc":"on loan", + "X_barcode":"33494647", + "X_author":[ + "Hof, Wim ¬[VerfasserIn]¬", + "Jong, Koen ¬de¬ ¬[VerfasserIn]¬" + ], + "X_medientyp":"B", + "X_exstatus":"NM", + "X_exstatus_desc":"Ausleihbar", + "X_shelfLocation":"YU 4000 H697", + "X_internal_date_issued":"65631,63260", + "X_date_issued":"2020-09-09 17:34:20", + "X_internal_date_due":"65659", + "X_date_due":"2020-10-07", + "X_days_to_due":"11", + "X_is_reserved":1, + "X_is_renewable":0, + "X_is_flrenewable":0, + "X_rsn":16612634 + }, + { + "status":3, + "about":"Matrix operations for engineers and scientists", + "label":"33220082", + "queue":"0", + "renewals":2, + "starttime":"2020-08-05T16:47:12Z", + "endtime":"2020-10-25T23:00:00Z", + "X_status_desc":"on loan", + "X_barcode":"33220082", + "X_author":[ + "Jeffrey, Alan" + ], + "X_medientyp":"B", + "X_exstatus":"N", + "X_exstatus_desc":"Ausleihbar", + "X_shelfLocation":"SK 220 J46", + "X_internal_date_issued":"65596,67632", + "X_date_issued":"2020-08-05 18:47:12", + "X_internal_date_due":"65678.", + "X_date_due":"2020-10-26", + "X_days_to_due":"30", + "X_is_reserved":0, + "X_is_renewable":1, + "X_is_flrenewable":0, + "X_internal_date_renewed":"65648", + "X_uni_date_renewed":"2020-09-25T22:00:00Z", + "X_date_renewed":"2020-09-26", + "X_rsn":14798304 + } + ] + }, + "fees":{ + "amount":"0,00 EUR", + "subset":"open", + "amount_list":"0,00 EUR", + "paid_all":"0,00 EUR", + "paid_list":"0,00 EUR", + "fee_all":"0,00 EUR", + "fee_list":"0,00 EUR", + "topay_all":"0,00 EUR", + "topay_list":"0,00 EUR", + "overdue_paid":"0,00 EUR", + "overdue_fee":"0,00 EUR", + "ill_paid":"0,00 EUR", + "ill_fee":"0,00 EUR", + "other_paid":"0,00 EUR", + "other_fee":"0,00 EUR", + "fee":[ + + ] + }, + "arguments":{ + "controller":"API", + "action":"account", + "username":"1234567" + } +} \ No newline at end of file diff --git a/opacclient/libopac/src/test/resources/slub/account/account.json b/opacclient/libopac/src/test/resources/slub/account/account.json index f1286a15c..6aa6d1bdd 100644 --- a/opacclient/libopac/src/test/resources/slub/account/account.json +++ b/opacclient/libopac/src/test/resources/slub/account/account.json @@ -84,7 +84,7 @@ "X_internal_date_due": "65161", "X_date_due": "2019-05-28", "X_days_to_due": "14", - "X_is_reserved": 1, + "X_is_reserved": 0, "X_is_renewable": 1, "X_is_flrenewable": 0, "X_rsn": 15864711 From 8f2cc2ce40d4b25ce8ff469e6d2f06d5baa5d8a2 Mon Sep 17 00:00:00 2001 From: StefRe Date: Tue, 29 Sep 2020 22:30:05 +0200 Subject: [PATCH 44/55] Remove colon from test resource file name to enable build on Windows systems as colons are not allowed in Windows file names. --- .../src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt | 2 +- ...UB Dresden: Startseite.html => SLUB Dresden Startseite.html} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename opacclient/libopac/src/test/resources/slub/{SLUB Dresden: Startseite.html => SLUB Dresden Startseite.html} (100%) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 980757110..743a5c157 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -887,7 +887,7 @@ class SLUBSearchFieldsMockTest : BaseHtmlTest() { @Test fun testParseSearchFields() { - val html = readResource("/slub/SLUB Dresden: Startseite.html") + val html = readResource("/slub/SLUB Dresden Startseite.html") Mockito.doReturn(html).`when`(slub).httpGet(Matchers.any(), Matchers.any()) val searchFields = slub.parseSearchFields() diff --git a/opacclient/libopac/src/test/resources/slub/SLUB Dresden: Startseite.html b/opacclient/libopac/src/test/resources/slub/SLUB Dresden Startseite.html similarity index 100% rename from opacclient/libopac/src/test/resources/slub/SLUB Dresden: Startseite.html rename to opacclient/libopac/src/test/resources/slub/SLUB Dresden Startseite.html From 1bfc8264cba1678121ba42da1fa1c7834eeefb52 Mon Sep 17 00:00:00 2001 From: StefRe Date: Tue, 29 Sep 2020 22:43:38 +0200 Subject: [PATCH 45/55] Replace ExpectedException.none() by assertThrows as the former became deprecated in JUnit 4.13 --- .../geeksfactory/opacclient/apis/SLUBTest.kt | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 743a5c157..c167f5bc9 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -37,9 +37,7 @@ import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat import org.json.JSONObject import org.junit.Assert.* -import org.junit.Rule import org.junit.Test -import org.junit.rules.ExpectedException import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Suite @@ -50,6 +48,7 @@ import org.mockito.Matchers.argThat import org.mockito.Matchers.eq import org.mockito.Mockito import org.mockito.Mockito.verify +import kotlin.collections.HashSet /** * Tests for SLUB API @@ -461,20 +460,18 @@ class SLUBAccountMockTest(@Suppress("unused") private val name: String, password = "x" } - @JvmField - @Rule - var thrown: ExpectedException = ExpectedException.none() - @Test fun testCheckAccountData() { Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) if (expectedException != null) { - thrown.expect(expectedException) - thrown.expectMessage(expectedExceptionMsg) + val thrown = assertThrows(expectedExceptionMsg, + expectedException, + { slub.requestAccount(account, "", null) }) + assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) + } else { + val actual = slub.requestAccount(account, "", null) + assertEquals(expectedMessage, actual.optString("message")) } - - val actual = slub.requestAccount(account, "", null) - assertEquals(expectedMessage, actual.optString("message")) } companion object { @@ -737,21 +734,19 @@ class SLUBSearchMockTest(@Suppress("unused") private val name: String, }, HttpClientFactory("test")) } - @JvmField - @Rule - var thrown: ExpectedException = ExpectedException.none() - @Test fun testSearch() { Mockito.doReturn(response).`when`(slub).httpGet(Matchers.any(), Matchers.any()) if (expectedException != null) { - thrown.expect(expectedException) - thrown.expectMessage(expectedExceptionMsg) + val thrown = assertThrows(expectedExceptionMsg, + expectedException, + { slub.search(query) }) + assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) + } else { + val actual = slub.search(query) + assertEquals(expectedResultCount, actual.total_result_count) + verify(slub).httpGet(expectedQueryUrl, "UTF-8") } - - val actual = slub.search(query) - assertEquals(expectedResultCount, actual.total_result_count) - verify(slub).httpGet(expectedQueryUrl, "UTF-8") } companion object { @@ -816,21 +811,19 @@ class SLUBProlongMockTest(@Suppress("unused") private val name: String, password = "x" } - @JvmField - @Rule - var thrown: ExpectedException = ExpectedException.none() - @Test fun testProlong() { Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) if (expectedException != null) { - thrown.expect(expectedException) - thrown.expectMessage(expectedExceptionMsg) + val thrown = assertThrows(expectedExceptionMsg, + expectedException, + { slub.prolong(media, account, 0, null) }) + assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) + } else { + val actualResult = slub.prolong(media, account, 0, null) + assertThat(actualResult, sameBeanAs(expectedResult)) + verify(slub).httpPost(eq(expectedQueryUrl), argThat(sameBeanAs(expectedRequestBody)), eq("UTF-8")) } - - val actualResult = slub.prolong(media, account, 0, null) - assertThat(actualResult, sameBeanAs(expectedResult)) - verify(slub).httpPost(eq(expectedQueryUrl), argThat(sameBeanAs(expectedRequestBody)), eq("UTF-8")) } companion object { From 87637d3678eff7305a5d049764a578d104789648 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Wed, 30 Sep 2020 16:18:21 +0200 Subject: [PATCH 46/55] Make lent items status message strings translatable --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 5 +++-- .../de/geeksfactory/opacclient/i18n/StringProvider.java | 2 ++ .../test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt | 6 +++++- .../opacapp/src/main/res/values-de/strings_api_errors.xml | 2 ++ .../opacapp/src/main/res/values/strings_api_errors.xml | 2 ++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 57d30ebe4..bcb6d277d 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -492,8 +492,9 @@ open class SLUB : OkHttpBaseApi() { } else { isRenewable = false status = when { - it.optInt("X_is_reserved") != 0 -> "vorgemerkt" // TODO: change to translatable string - it.optInt("renewals") > 0 -> "${it.optInt("renewals")}x verlängert" // TODO: change to translatable string + it.optInt("X_is_reserved") != 0 -> stringProvider.getString(StringProvider.RESERVED) + it.optInt("renewals") > 0 -> stringProvider.getFormattedString( + StringProvider.RENEWED, it.optInt("renewals")) else -> null } } diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java index a97eba9ec..8dc022bec 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java @@ -93,6 +93,8 @@ public interface StringProvider { String REQUEST_PROGRESS = "request_progress"; String RESERVED_POS = "reserved_pos"; String COPY = "copy"; + String RESERVED = "reserved"; + String RENEWED = "renewed"; /** * Returns the translated string identified by identifier diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index c167f5bc9..5f5f79eed 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -70,7 +70,10 @@ class SLUBAllTests private class TestStringProvider : StringProvider { override fun getString(identifier: String?): String { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + return when (identifier) { + RESERVED -> "vorgemerkt" + else -> identifier!! + } } override fun getFormattedString(identifier: String?, vararg args: Any?): String { @@ -78,6 +81,7 @@ private class TestStringProvider : StringProvider { RESERVED_POS -> String.format("vorgemerkt, Pos. %s", *args) HOLD -> String.format("liegt seit %s bereit", *args) REQUEST_READY -> String.format("seit %s abholbereit (Magazinbestellung)", *args) + RENEWED -> String.format("%dx verlängert", *args) else -> identifier!! } } diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index ed20980b7..d4e80286c 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -93,4 +93,6 @@ Magazinbestellung seit %s in Arbeit vorgemerkt, Pos. %s Exemplar + vorgemerkt + %dx verlängert diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index b1dbf6ee7..e267ed2c0 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -94,4 +94,6 @@ stack request in progress since %s reserved, pos. %s Copy + reserved + %dx renewed From b5ea66f3177bf225963d520972d7c16167634c1d Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Wed, 30 Sep 2020 16:35:00 +0200 Subject: [PATCH 47/55] Fix code analysis warning overriding '@string/copy' which is marked as private in androidx.preference:preference. --- .../opacapp/src/main/res/values-de/strings_api_errors.xml | 4 ++-- opacclient/opacapp/src/main/res/values/strings_api_errors.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index d4e80286c..84aa56aa7 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -1,5 +1,5 @@ - + Diese Bibliothek unterstützt nur bis zu ein benutztes Suchkriterium. @@ -92,7 +92,7 @@ liegt seit %s zur Benutzung in Bibliothek bereit Magazinbestellung seit %s in Arbeit vorgemerkt, Pos. %s - Exemplar + Exemplar vorgemerkt %dx verlängert diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index e267ed2c0..ec6131ca0 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -93,7 +93,7 @@ ready since %s for usage in library stack request in progress since %s reserved, pos. %s - Copy + Copy reserved %dx renewed From 3b7ca5e21fff3f4dab68185625d958fc721d20e9 Mon Sep 17 00:00:00 2001 From: StefRe Date: Wed, 30 Sep 2020 21:10:41 +0200 Subject: [PATCH 48/55] Rename string COPY to ITEM_COPY to prevent overriding '@string/copy' which is marked as private in androidx.preference:preference. This basically reverts commit b5ea66f and prevents the warning by renaming instead of marking the string as overriding. --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 2 +- .../java/de/geeksfactory/opacclient/i18n/StringProvider.java | 2 +- .../src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt | 2 +- .../opacapp/src/main/res/values-de/strings_api_errors.xml | 4 ++-- opacclient/opacapp/src/main/res/values/strings_api_errors.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index bcb6d277d..85200c3d7 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -301,7 +301,7 @@ open class SLUB : OkHttpBaseApi() { "value" to "${copy.branch}: ${copy.status}") } return OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, - stringProvider.getString(StringProvider.COPY)).apply { + stringProvider.getString(StringProvider.ITEM_COPY)).apply { actionIdentifier = ACTION_COPY this.selection = options } diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java index 8dc022bec..6d5c1cc07 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/i18n/StringProvider.java @@ -92,7 +92,7 @@ public interface StringProvider { String READINGROOM = "readingroom"; String REQUEST_PROGRESS = "request_progress"; String RESERVED_POS = "reserved_pos"; - String COPY = "copy"; + String ITEM_COPY = "item_copy"; String RESERVED = "reserved"; String RENEWED = "renewed"; diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 5f5f79eed..505d6f730 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -638,7 +638,7 @@ class SLUBReservationMockTest(@Suppress("unused") private val name: String, null, null, null, - OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, "copy").apply { + OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.SELECTION_NEEDED, "item_copy").apply { actionIdentifier = OpacApi.ReservationResult.ACTION_USER + 1 this.selection = listOf( mapOf("key" to "reserve\t10059731", "value" to "Zentralbibliothek: Ausgeliehen, Vormerken möglich"), diff --git a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml index 84aa56aa7..ad7ad967d 100644 --- a/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values-de/strings_api_errors.xml @@ -1,5 +1,5 @@ - + Diese Bibliothek unterstützt nur bis zu ein benutztes Suchkriterium. @@ -92,7 +92,7 @@ liegt seit %s zur Benutzung in Bibliothek bereit Magazinbestellung seit %s in Arbeit vorgemerkt, Pos. %s - Exemplar + Exemplar vorgemerkt %dx verlängert diff --git a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml index ec6131ca0..2fb407703 100644 --- a/opacclient/opacapp/src/main/res/values/strings_api_errors.xml +++ b/opacclient/opacapp/src/main/res/values/strings_api_errors.xml @@ -93,7 +93,7 @@ ready since %s for usage in library stack request in progress since %s reserved, pos. %s - Copy + Copy reserved %dx renewed From db35f1120792642ba436aa381fb36855fe8d6307 Mon Sep 17 00:00:00 2001 From: StefRe Date: Fri, 9 Oct 2020 20:47:07 +0200 Subject: [PATCH 49/55] Fix typo and coding conventions issue (lambda argument should be moved out of parentheses) --- .../de/geeksfactory/opacclient/apis/SLUBTest.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 505d6f730..5070190cf 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -468,9 +468,8 @@ class SLUBAccountMockTest(@Suppress("unused") private val name: String, fun testCheckAccountData() { Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) if (expectedException != null) { - val thrown = assertThrows(expectedExceptionMsg, - expectedException, - { slub.requestAccount(account, "", null) }) + val thrown = assertThrows(expectedExceptionMsg, expectedException + ) { slub.requestAccount(account, "", null) } assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) } else { val actual = slub.requestAccount(account, "", null) @@ -671,7 +670,7 @@ class SLUBReservationMockTest(@Suppress("unused") private val name: String, "{\"status\":true,\"message\":\"Magazinbestellung wurde erfolgreich hinzugefügt.\",\"requestID\":\"2116982\",\"pickupPoint\":\"Zentralbibliothek Ebene 0 SB-Regal\",\"arguments\":{\"controller\":\"API\",\"action\":\"stackRequest\",\"barcode\":\"20065307\",\"username\":\"123456\",\"pickupPoint\":\"a01\"}}", OpacApi.ReservationResult(OpacApi.MultiStepResult.Status.OK, "Magazinbestellung wurde erfolgreich hinzugefügt.") ), - // "selected requestable copy wiht multiple pickup ponits" doesn't need to be tested as it's the same process as "Selected reservable copy (with multiple pickup branches)" + // "selected requestable copy with multiple pickup points" doesn't need to be tested as it's the same process as "Selected reservable copy (with multiple pickup branches)" arrayOf("No requestable or reservable copies", itemNone, 0, @@ -742,9 +741,8 @@ class SLUBSearchMockTest(@Suppress("unused") private val name: String, fun testSearch() { Mockito.doReturn(response).`when`(slub).httpGet(Matchers.any(), Matchers.any()) if (expectedException != null) { - val thrown = assertThrows(expectedExceptionMsg, - expectedException, - { slub.search(query) }) + val thrown = assertThrows(expectedExceptionMsg, expectedException + ) { slub.search(query) } assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) } else { val actual = slub.search(query) @@ -819,9 +817,8 @@ class SLUBProlongMockTest(@Suppress("unused") private val name: String, fun testProlong() { Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) if (expectedException != null) { - val thrown = assertThrows(expectedExceptionMsg, - expectedException, - { slub.prolong(media, account, 0, null) }) + val thrown = assertThrows(expectedExceptionMsg, expectedException + ) { slub.prolong(media, account, 0, null) } assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) } else { val actualResult = slub.prolong(media, account, 0, null) From 08dfa091c944603cda0a76cd025fb2c200d7ba14 Mon Sep 17 00:00:00 2001 From: StefRe Date: Fri, 9 Oct 2020 23:08:37 +0200 Subject: [PATCH 50/55] Add tests for Cancel --- .../geeksfactory/opacclient/apis/SLUBTest.kt | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 5070190cf..be0fd9a3c 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -64,7 +64,8 @@ import kotlin.collections.HashSet SLUBAccountValidateMockTest::class, SLUBSearchMockTest::class, SLUBSearchFieldsMockTest::class, - SLUBProlongMockTest::class + SLUBProlongMockTest::class, + SLUBCancelMockTest::class ) class SLUBAllTests @@ -867,6 +868,62 @@ class SLUBProlongMockTest(@Suppress("unused") private val name: String, } } +@RunWith(Parameterized::class) +class SLUBCancelMockTest(@Suppress("unused") private val name: String, + private val media: String, + private val response: String?, + private val expectedResult: OpacApi.MultiStepResult) : BaseHtmlTest() { + private val slub = Mockito.spy(SLUB::class.java) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "https://test.de") + put("illrenewurl", "https://test-renew.de") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "123456" + password = "x" + } + + @Test + fun testCancel() { + val expectedRequestBody = FormBody.Builder() + .add("type", "1") + .add("tx_slubaccount_account[controller]", "API") + .add("tx_slubaccount_account[action]", "delete") + .add("tx_slubaccount_account[username]", "123456") + .add("tx_slubaccount_account[password]", "x") + .add("tx_slubaccount_account[delete][0]", media) + .build() + Mockito.doReturn(response).`when`(slub).httpPost(Matchers.any(), Matchers.any(), Matchers.any()) + val actualResult = slub.cancel(media, account, 0, null) + assertThat(actualResult, sameBeanAs(expectedResult)) + verify(slub).httpPost(eq("https://test.de/mein-konto/"), argThat(sameBeanAs(expectedRequestBody)), eq("UTF-8")) + } + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = listOf( + arrayOf("OK", + "31481285_1", + "{\"status\":1,\"message\":\"Reservation Deleted\",\"arguments\":{\"controller\":\"API\",\"action\":\"delete\",\"username\":\"123456\",\"delete\":[\"31481285_1\"]}} ", + OpacApi.CancelResult(OpacApi.MultiStepResult.Status.OK) + ), + arrayOf("Error (item was not reserved)", + "31481285_1", + "{\"status\":\"-1\",\"message\":\"Item not reserved\",\"arguments\":{\"controller\":\"API\",\"action\":\"delete\",\"username\":\"123456\",\"delete\":[\"31481285_1\"]}}", + OpacApi.CancelResult(OpacApi.MultiStepResult.Status.ERROR, "unknown_error_account_with_description Item not reserved") + ) + ) + } +} + class SLUBSearchFieldsMockTest : BaseHtmlTest() { private val slub = Mockito.spy(SLUB::class.java) From eedeeba13827931faaf275e5a66b7aacf93c795d Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Mon, 12 Oct 2020 09:08:29 +0200 Subject: [PATCH 51/55] Use DummyStringProvider for testing --- .../geeksfactory/opacclient/apis/SLUBTest.kt | 42 ++++--------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index be0fd9a3c..72166d4ca 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -22,8 +22,7 @@ package de.geeksfactory.opacclient.apis import com.shazam.shazamcrest.matcher.Matchers.sameBeanAs -import de.geeksfactory.opacclient.i18n.StringProvider -import de.geeksfactory.opacclient.i18n.StringProvider.* +import de.geeksfactory.opacclient.i18n.DummyStringProvider import de.geeksfactory.opacclient.networking.HttpClientFactory import de.geeksfactory.opacclient.objects.* import de.geeksfactory.opacclient.searchfields.DropdownSearchField @@ -69,38 +68,11 @@ import kotlin.collections.HashSet ) class SLUBAllTests -private class TestStringProvider : StringProvider { - override fun getString(identifier: String?): String { - return when (identifier) { - RESERVED -> "vorgemerkt" - else -> identifier!! - } - } - - override fun getFormattedString(identifier: String?, vararg args: Any?): String { - return when (identifier) { - RESERVED_POS -> String.format("vorgemerkt, Pos. %s", *args) - HOLD -> String.format("liegt seit %s bereit", *args) - REQUEST_READY -> String.format("seit %s abholbereit (Magazinbestellung)", *args) - RENEWED -> String.format("%dx verlängert", *args) - else -> identifier!! - } - } - - override fun getQuantityString(identifier: String?, count: Int, vararg args: Any?): String { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun getMediaTypeName(mediaType: SearchResult.MediaType?): String { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } -} - class SLUBAccountTest : BaseHtmlTest() { private var slub = SLUB() init { - slub.stringProvider = TestStringProvider() + slub.stringProvider = DummyStringProvider() } @Test @@ -135,7 +107,7 @@ class SLUBAccountTest : BaseHtmlTest() { author = "Mitchell, Alan" format = "B" //id = "30963742" - status = "vorgemerkt, Pos. 1" + status = "reserved_pos 1" cancelData = "30963742_1" } val reserveditem2 = ReservedItem().apply { @@ -145,7 +117,7 @@ class SLUBAccountTest : BaseHtmlTest() { format = "B" //id = "34778398" branch = "ZwB Forstwissenschaft" - status = String.format("liegt seit %s bereit", fmt.print(LocalDate("2019-05-10"))) + status = String.format("hold %s", fmt.print(LocalDate("2019-05-10"))) } val reserveditem3 = ReservedItem().apply { // request ready @@ -154,7 +126,7 @@ class SLUBAccountTest : BaseHtmlTest() { format = "B" //id = "20550495" branch = "Zentralbibliothek Ebene 0 SB-Regal" - status = String.format("seit %s abholbereit (Magazinbestellung)", fmt.print(LocalDate("2019-05-04"))) + status = String.format("request_ready %s", fmt.print(LocalDate("2019-05-04"))) } val accountdata = slub.parseAccountData(Account(), json) @@ -189,8 +161,8 @@ class SLUBAccountTest : BaseHtmlTest() { val accountdata = slub.parseAccountData(Account(), json) - assertEquals("9x verlängert", accountdata.lent[0].status) - assertEquals("vorgemerkt", accountdata.lent[1].status) + assertEquals("renewed 9", accountdata.lent[0].status) + assertEquals("reserved", accountdata.lent[1].status) assertEquals(null, accountdata.lent[2].status) assertEquals(false, accountdata.lent[0].isRenewable) assertEquals(false, accountdata.lent[1].isRenewable) From a18564d48992364d456bec22822386c99edb3885 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Mon, 12 Oct 2020 09:17:17 +0200 Subject: [PATCH 52/55] Add comment about FL = inter-library loan --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 85200c3d7..fe4052296 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -379,7 +379,7 @@ open class SLUB : OkHttpBaseApi() { OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.ERROR, "internal error") } return try { - if (data[0] == "FL") { + if (data[0] == "FL") { // inter-library loan (FL = Fernleihe) renewal request val formBody = FormBody.Builder() .add("bc", data[1]) .add("uid", account.name) @@ -388,7 +388,7 @@ open class SLUB : OkHttpBaseApi() { .build() val result = httpPost(illRenewUrl, formBody, ENCODING).html OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK, result.select("p:last-of-type").text()) - } else { + } else { // regular, i.e. SLUB item renewal requestAccount(account, "renew", mapOf("tx_slubaccount_account[renewals][0]" to data[1])) OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.OK) } From c81415af737ec90244fec4d00b68a867f283bfea Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Mon, 12 Oct 2020 10:58:55 +0200 Subject: [PATCH 53/55] Simplify code by replacing .map with .forEach --- .../src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index fe4052296..adb32d241 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -511,9 +511,7 @@ open class SLUB : OkHttpBaseApi() { .add("tx_slubaccount_account[action]", action) .add("tx_slubaccount_account[username]", account.name) .add("tx_slubaccount_account[password]", account.password) - parameters?.map { - formBody.add(it.key, it.value) - } + parameters?.forEach { formBody.add(it.key, it.value) } try { return JSONObject(httpPost("$baseurl/mein-konto/", formBody.build(), ENCODING)).also { if (!(it.optInt("status") == 1 || it.optBoolean("status"))) { From 6d6d6021b1b65987acb64dfb45084d46811439a8 Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Tue, 13 Oct 2020 15:21:08 +0200 Subject: [PATCH 54/55] Change JSON getters from opt to get in cases where the item is always available. --- .../de/geeksfactory/opacclient/apis/SLUB.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index adb32d241..3f858230e 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -138,12 +138,11 @@ open class SLUB : OkHttpBaseApi() { } internal fun parseSearchResults(json: JSONObject): SearchRequestResult { - val searchresults = json.optJSONArray("docs")?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } - ?.map { + val searchresults = json.getJSONArray("docs").let { 0.until(it.length()).map { i -> it.getJSONObject(i) } } + .map { SearchResult().apply { - innerhtml = "${it.optString("title")}
${it.optJSONArray("author")?.optString(0) - ?: ""}" - it.optString("creationDate")?.run { + innerhtml = "${it.getString("title")}
${it.getJSONArray("author").optString(0)}" + it.getString("creationDate").run { if (this != "null") { innerhtml += "
(${this})" } @@ -153,7 +152,7 @@ open class SLUB : OkHttpBaseApi() { } } //TODO: get status (one request per item!) - return SearchRequestResult(searchresults, json.optInt("numFound"), 1) + return SearchRequestResult(searchresults, json.getInt("numFound"), 1) } override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { @@ -206,7 +205,7 @@ open class SLUB : OkHttpBaseApi() { } return DetailedItem().apply { this.id = id - val record = json.optJSONObject("record") + val record = json.getJSONObject("record") for (key in record.keys()) { var value = when (val v = record.get(key as String)) { is String -> v @@ -231,14 +230,14 @@ open class SLUB : OkHttpBaseApi() { addDetail(Detail(fieldCaptions[key], value)) } } - json.optString("thumbnail")?.run { + json.getString("thumbnail")?.run { if (this != "") { cover = this } } // links and references for (link in listOf("linksRelated", "linksAccess", "linksGeneral")) { - val linkArray = json.optJSONArray(link) + val linkArray = json.getJSONArray(link) linkArray.run { 0.until(length()).map { optJSONObject(it) } }.map { // assuming that only on of material, note or hostlabel is set val key = with(it.optString("material") + it.optString("note") + it.optString("hostLabel")) { @@ -247,12 +246,12 @@ open class SLUB : OkHttpBaseApi() { addDetail(Detail(key, it.optString("uri"))) } } - json.optJSONArray("references").run { 0.until(length()).map { optJSONObject(it) } }.map { + json.getJSONArray("references").run { 0.until(length()).map { optJSONObject(it) } }.map { // TODO: usually links to old SLUB catalogue, does it make sense to add the link? addDetail(Detail(it.optString("text"), "${it.optString("name")} (${it.optString("target")})")) } // copies - val cps = json.opt("copies") + val cps = json.get("copies") if (cps is JSONArray) { getCopies(cps, dateFormat).let { copies = it } } else { // multiple arrays @@ -473,8 +472,8 @@ open class SLUB : OkHttpBaseApi() { } return AccountData(account.id).apply { - pendingFees = json.optJSONObject("fees")?.optString("topay_list") - validUntil = json.optJSONObject("memberInfo")?.optString("expires") + pendingFees = json.getJSONObject("fees").getString("topay_list") + validUntil = json.getJSONObject("memberInfo").getString("expires") ?.substring(0, 10)?.let { fmt.print(LocalDate(it)) } lent = json.optJSONObject("items")?.optJSONArray("loan") // TODO: plus permanent loans? (need example) ?.run { 0.until(length()).map { optJSONObject(it) } } From b518431edc3dcd7d54bc1487062d29edc31a80ea Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Tue, 13 Oct 2020 19:00:23 +0200 Subject: [PATCH 55/55] Set SearchResult.MediaType to NONE for unknown formats --- .../java/de/geeksfactory/opacclient/apis/SLUB.kt | 5 +++-- .../de/geeksfactory/opacclient/apis/SLUBTest.kt | 6 ++++++ .../resources/slub/search/simple-search.json | 16 ++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt index 3f858230e..9bf646c47 100644 --- a/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -147,8 +147,9 @@ open class SLUB : OkHttpBaseApi() { innerhtml += "
(${this})" } } - type = mediaTypes[it.optJSONArray("format")?.optString(0)] - id = it.optString("id") + type = mediaTypes[it.getJSONArray("format").optString(0)] + ?: SearchResult.MediaType.NONE + id = it.getString("id") } } //TODO: get status (one request per item!) diff --git a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt index 72166d4ca..5ade3b7b3 100644 --- a/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -191,11 +191,17 @@ class SLUBSearchTest : BaseHtmlTest() { type = SearchResult.MediaType.BOOK id = "0-1014939550" } + val result2 = SearchResult().apply { + innerhtml = """Title with " and &

(2222)""" + type = SearchResult.MediaType.NONE + id = "123" + } val searchresults = slub.parseSearchResults(json) assertEquals(2, searchresults.total_result_count) assertThat(result1, samePropertyValuesAs(searchresults.results[0])) + assertThat(result2, samePropertyValuesAs(searchresults.results[1])) } @Test diff --git a/opacclient/libopac/src/test/resources/slub/search/simple-search.json b/opacclient/libopac/src/test/resources/slub/search/simple-search.json index 689fe67f5..8593a62d9 100644 --- a/opacclient/libopac/src/test/resources/slub/search/simple-search.json +++ b/opacclient/libopac/src/test/resources/slub/search/simple-search.json @@ -17,18 +17,14 @@ ] }, { - "id": "dswarm-141-NzE4NjM5", + "id": "123", "format": [ - "Video" + "???" ], - "title": "Learning JUnit 5", - "author": [ - "Boni García" - ], - "creationDate": "2018", - "imprint": [ - "Carpenteria : Lynda, 2018" - ] + "title": "Title with \" and &", + "author": [], + "creationDate": "2222", + "imprint": [] } ], "facets": {