From 9ed8476983f343660ed5f1a3d39a7aabab8ec07b Mon Sep 17 00:00:00 2001 From: Steffen Rehberg Date: Thu, 15 Oct 2020 08:25:57 +0200 Subject: [PATCH] Implement support for SLUB Dresden (#553) * Add SLUB API, implement parseSearchFields and getSupportFlags This API is specific for SLUB in Dresden. * Implement search, searchGetPage, getResultById, prolong, account, checkAccountData * Implement getShareUrl * Add two more mediatypes * Refactor for testing, add tests for account, search result and detailed item * Include year in search results only if not "null" * Correctly process reserved inter-library loan items * Make test of expiry date work independently of locale Format expected date value according to current locale (required for CI testing) * implement cancel * 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). * Add MIT license header as requested at https://github.com/opacapp/opacclient/wiki * 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. * Add remaining details to DetailedItem and fix reserved and lent items * Add SUPPORT_FLAG_ENDLESS_SCROLLING flag to prevent reloading the page the selected result is located on before calling getResultById. * 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) * Process boolean status values in account request results and add test for requestAccount * Implement reservation and tests for it - minor fix to SLUBAccountMockTest - combine all SLUB tests into test suite * Fix code analysis issues and simplify isRenewable assignment * Refactor mapping of json array to map for pickup points to simplify code * Add SLUB API, implement parseSearchFields and getSupportFlags This API is specific for SLUB in Dresden. * Implement search, searchGetPage, getResultById, prolong, account, checkAccountData * Implement getShareUrl * Add two more mediatypes * Refactor for testing, add tests for account, search result and detailed item * Include year in search results only if not "null" * Correctly process reserved inter-library loan items * Make test of expiry date work independently of locale Format expected date value according to current locale (required for CI testing) * implement cancel * 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). * Add MIT license header as requested at https://github.com/opacapp/opacclient/wiki * 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. * Add remaining details to DetailedItem and fix reserved and lent items * Add SUPPORT_FLAG_ENDLESS_SCROLLING flag to prevent reloading the page the selected result is located on before calling getResultById. * 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) * Process boolean status values in account request results and add test for requestAccount * Implement reservation and tests for it - minor fix to SLUBAccountMockTest - combine all SLUB tests into test suite * Fix code analysis issues and simplify isRenewable assignment * 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 * Refactor mapping of json array to map for pickup points to simplify code * Implement renewal for inter-library loan items and tests for it * Fix various code formatting issues * 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. * 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) * Remove colon from test resource file name to enable build on Windows systems as colons are not allowed in Windows file names. * Replace ExpectedException.none() by assertThrows as the former became deprecated in JUnit 4.13 * Make lent items status message strings translatable * Fix code analysis warning overriding '@string/copy' which is marked as private in androidx.preference:preference. * 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. * Fix typo and coding conventions issue (lambda argument should be moved out of parentheses) * Add tests for Cancel * Use DummyStringProvider for testing * Add comment about FL = inter-library loan * Simplify code by replacing .map with .forEach * Change JSON getters from opt to get in cases where the item is always available. * Set SearchResult.MediaType to NONE for unknown formats --- opacclient/libopac/build.gradle | 1 + .../opacclient/OpacApiFactory.java | 3 + .../de/geeksfactory/opacclient/apis/SLUB.kt | 569 +++++ .../opacclient/i18n/StringProvider.java | 8 + .../src/main/resources/meanings/slub.json | 3 + .../geeksfactory/opacclient/apis/SLUBTest.kt | 950 +++++++++ .../slub/SLUB Dresden Startseite.html | 1874 +++++++++++++++++ .../slub/account/account-ill_in_progress.json | 76 + .../slub/account/account-status.json | 153 ++ .../test/resources/slub/account/account.json | 190 ++ .../resources/slub/account/empty-account.json | 58 + .../resources/slub/account/ill-renew.html | 36 + .../resources/slub/search/empty-search.json | 47 + .../item-copies_in_multiple_arrays.json | 400 ++++ .../slub/search/item-for-reserve&request.json | 98 + .../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 ++ .../search/search-null_creation_date.json | 111 + .../resources/slub/search/simple-item.json | 78 + .../resources/slub/search/simple-search.json | 127 ++ .../main/res/values-de/strings_api_errors.xml | 8 + .../main/res/values/strings_api_errors.xml | 8 + 25 files changed, 5276 insertions(+) create mode 100644 opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt create mode 100644 opacclient/libopac/src/main/resources/meanings/slub.json create mode 100644 opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt create mode 100644 opacclient/libopac/src/test/resources/slub/SLUB Dresden Startseite.html create mode 100644 opacclient/libopac/src/test/resources/slub/account/account-ill_in_progress.json create mode 100644 opacclient/libopac/src/test/resources/slub/account/account-status.json 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/account/ill-renew.html 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/item-for-reserve&request.json 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 create mode 100644 opacclient/libopac/src/test/resources/slub/search/search-null_creation_date.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/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..9bf646c47 --- /dev/null +++ b/opacclient/libopac/src/main/java/de/geeksfactory/opacclient/apis/SLUB.kt @@ -0,0 +1,569 @@ +/* + * 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 +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 +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 +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 + * + * @author Steffen Rehberg, Jan 2019 + */ +open class SLUB : OkHttpBaseApi() { + protected lateinit var baseurl: String + protected lateinit var illRenewUrl: String + private 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, + "Electronic Resource (Data Carrier)" to SearchResult.MediaType.EAUDIO, + "Image" to SearchResult.MediaType.ART, + "Microform" to SearchResult.MediaType.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", + "linksRelated" to "Info", + "linksAccess" to "Zugang", + "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") + illRenewUrl = library.data.getString("illrenewurl") + } + + override fun search(query: List): SearchRequestResult { + this.query = query + return searchGetPage(1) + } + + override fun searchGetPage(page: Int): SearchRequestResult { + val queryUrlB = HttpUrl.get("$baseurl/?type=1369315142&tx_find_find[format]=data&tx_find_find[data-format]=app") + .newBuilder() + .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 { + 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(queryUrl.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 { + val searchresults = json.getJSONArray("docs").let { 0.until(it.length()).map { i -> it.getJSONObject(i) } } + .map { + SearchResult().apply { + innerhtml = "${it.getString("title")}
${it.getJSONArray("author").optString(0)}" + it.getString("creationDate").run { + if (this != "null") { + innerhtml += "
(${this})" + } + } + type = mediaTypes[it.getJSONArray("format").optString(0)] + ?: SearchResult.MediaType.NONE + id = it.getString("id") + } + } + //TODO: get status (one request per item!) + return SearchRequestResult(searchresults, json.getInt("numFound"), 1) + } + + override fun filterResults(filter: Filter, option: Filter.Option): SearchRequestResult { + TODO("not implemented") + } + + override fun getResultById(id: String, homebranch: String?): DetailedItem { + 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) + } + + 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 { + 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) + } + } + // 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 = "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 + } + } + return DetailedItem().apply { + this.id = id + val record = json.getJSONObject("record") + for (key in record.keys()) { + var value = when (val v = record.get(key as String)) { + is String -> v + is Int -> v.toString() + is JSONArray -> 0.until(v.length()).map { + 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 + collectionId = arrayItem.optString(("id"), null) + } + else -> null + } + }.joinToString("; ") + else -> "" + } + if (value.isNotEmpty()) { + value = Parser.unescapeEntities(value, false) + if (key == "title") { + title = value + } + addDetail(Detail(fieldCaptions[key], value)) + } + } + json.getString("thumbnail")?.run { + if (this != "") { + cover = this + } + } + // links and references + for (link in listOf("linksRelated", "linksAccess", "linksGeneral")) { + 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")) { + if (isEmpty()) fieldCaptions[link] else this + } + addDetail(Detail(key, it.optString("uri"))) + } + } + 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.get("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 + } + isReservable = hasReservableCopies + // 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() + } + } + + override fun getResult(position: Int): DetailedItem? { + // not used (getResultById is implemented and every search result has an id set) + return null + } + + override fun reservation(item: DetailedItem, account: Account, useraction: Int, selection: String?): OpacApi.ReservationResult { + 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.ITEM_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) }.associateWith { + pickupPoints.getOrElse(it) { it } + } + } ?: 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 { + val data = media.split('\t') + if (data.size != 2) { + OpacApi.ProlongResult(OpacApi.MultiStepResult.Status.ERROR, "internal error") + } + return try { + if (data[0] == "FL") { // inter-library loan (FL = Fernleihe) renewal request + 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 { // regular, i.e. SLUB item renewal + 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) + } + } + + override fun prolongAll(account: Account, useraction: Int, selection: String?): OpacApi.ProlongAllResult { + return OpacApi.ProlongAllResult(OpacApi.MultiStepResult.Status.UNSUPPORTED) + } + + override fun cancel(media: String, account: Account, useraction: Int, selection: String?): OpacApi.CancelResult { + 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 { + val json = requestAccount(account, "account") + return parseAccountData(account, json) + } + + 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" + 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) + //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 + "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")}" + } + } + }) + } + } + } + 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") --> 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") + } + }) + } + } + + return reservationsList + } + + return AccountData(account.id).apply { + 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) } } + ?.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") + //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") + 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 -> stringProvider.getString(StringProvider.RESERVED) + it.optInt("renewals") > 0 -> stringProvider.getFormattedString( + StringProvider.RENEWED, it.optInt("renewals")) + else -> null + } + } + } + } ?: emptyList() + reservations = getReservations(json.optJSONObject("items")) + } + } + + internal 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?.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"))) { + 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) { + requestAccount(account, "validate") + } + + override fun getShareUrl(id: String?, title: String?): String { + return "$baseurl/id/$id" + } + + override fun getSupportFlags(): Int { + return OpacApi.SUPPORT_FLAG_ENDLESS_SCROLLING + } + + 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-recreated-list li").map { + TextSearchField().apply { + id = it["id"] + 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?) { + //TODO("not implemented") + } + +} 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..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 @@ -87,6 +87,14 @@ 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"; + String ITEM_COPY = "item_copy"; + String RESERVED = "reserved"; + String RENEWED = "renewed"; /** * Returns the translated string identified by identifier 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 new file mode 100644 index 000000000..5ade3b7b3 --- /dev/null +++ b/opacclient/libopac/src/test/java/de/geeksfactory/opacclient/apis/SLUBTest.kt @@ -0,0 +1,950 @@ +/* + * 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 +import de.geeksfactory.opacclient.i18n.DummyStringProvider +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 +import org.hamcrest.Matchers.* +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat +import org.json.JSONObject +import org.junit.Assert.* +import org.junit.Test +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.Matchers.eq +import org.mockito.Mockito +import org.mockito.Mockito.verify +import kotlin.collections.HashSet + +/** + * Tests for SLUB API + * + * @author Steffen Rehberg, Jan 2020 + */ +@RunWith(Suite::class) +@Suite.SuiteClasses( + SLUBAccountTest::class, + SLUBSearchTest::class, + SLUBAccountMockTest::class, + SLUBReservationMockTest::class, + SLUBAccountValidateMockTest::class, + SLUBSearchMockTest::class, + SLUBSearchFieldsMockTest::class, + SLUBProlongMockTest::class, + SLUBCancelMockTest::class +) +class SLUBAllTests + +class SLUBAccountTest : BaseHtmlTest() { + private var slub = SLUB() + + init { + slub.stringProvider = DummyStringProvider() + } + + @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(DateTimeFormat.shortDate().print(LocalDate("2020-03-31")), accountdata.validUntil) + assertTrue(accountdata.lent.isEmpty()) + assertTrue(accountdata.reservations.isEmpty()) + } + + @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 = "$format\t$barcode" + } + val reserveditem1 = ReservedItem().apply { + // reserve + title = "Pareys Buch der Bäume" + author = "Mitchell, Alan" + format = "B" + //id = "30963742" + status = "reserved_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("hold %s", 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("request_ready %s", 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])) + assertThat(accountdata.reservations, hasItems(sameBeanAs(reserveditem1), + sameBeanAs(reserveditem2), sameBeanAs(reserveditem3))) + } + + @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])) + } + + @Test + fun testParseAccountDataStatus() { + val json = JSONObject(readResource("/slub/account/account-status.json")) + + val accountdata = slub.parseAccountData(Account(), json) + + 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) + assertEquals(true, accountdata.lent[2].isRenewable) + } +} + +class SLUBSearchTest : BaseHtmlTest() { + private 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 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 + 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")) + 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")) + addDetail(Detail("Inhaltsverzeichnis", "http://d-nb.info/970689268/04")) + id = "0-1182402208" + copies = arrayListOf(Copy().apply { + barcode = "31541466" + department = "Freihand" + branch = "Bereichsbibliothek DrePunct" + status = "Ausleihbar" + shelfmark = "ST 233 H939" + }) + collectionId = "0-1183957874" + } + + 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" + resInfo = "stackRequest\t10418078" + } + 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" + resInfo = "stackRequest\t33364639" + } + + 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))) + } + + @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))) + } + + @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(@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 { + put("baseurl", "https://test.de") + put("illrenewurl", "https://test-renew.de") + } + }, HttpClientFactory("test")) + } + + private val account = Account().apply { + name = "x" + password = "x" + } + + @Test + 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) } + assertTrue(thrown!!.message!!.contains(expectedExceptionMsg!!)) + } else { + val actual = slub.requestAccount(account, "", null) + assertEquals(expectedMessage, actual.optString("message")) + } + } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = listOf( + // validate: status as string + 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("Malformed", ".", null, OpacApi.OpacErrorException::class.java, "Request didn't return JSON object"), + // delete: status as int or string + 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("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") + ) + } +} + +@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") + put("illrenewurl", "https://test-renew.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, "item_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 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, + 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") + put("illrenewurl", "https://test-renew.de") + } + }, 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()) + } +} + +@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") + put("illrenewurl", "https://test-renew.de") + } + }, HttpClientFactory("test")) + } + + @Test + fun testSearch() { + Mockito.doReturn(response).`when`(slub).httpGet(Matchers.any(), Matchers.any()) + if (expectedException != null) { + 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") + } + } + + 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 + ) + ) + } +} + +@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" + } + + @Test + 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) } + 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")) + } + } + + 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 + ) + ) + } +} + +@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) + + init { + slub.init(Library().apply { + data = JSONObject().apply { + put("baseurl", "test") + put("illrenewurl", "https://test-renew.de") + } + }, HttpClientFactory("test")) + } + + @Test + fun testParseSearchFields() { + val html = readResource("/slub/SLUB Dresden Startseite.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? + 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/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 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 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 new file mode 100644 index 000000000..6aa6d1bdd --- /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": 0, + "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/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 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/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/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/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 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..8593a62d9 --- /dev/null +++ b/opacclient/libopac/src/test/resources/slub/search/simple-search.json @@ -0,0 +1,127 @@ +{ + "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": "123", + "format": [ + "???" + ], + "title": "Title with \" and &", + "author": [], + "creationDate": "2222", + "imprint": [] + } + ], + "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 + } + } + } + } +} 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..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 @@ -87,4 +87,12 @@ 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 + 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 09f494060..2fb407703 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,12 @@ 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 + Copy + reserved + %dx renewed