package de.geeksfactory.opacclient.apis; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.message.BasicNameValuePair; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.json.JSONException; import org.json.JSONObject; import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.FormElement; import org.jsoup.select.Elements; import java.io.IOException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import de.geeksfactory.opacclient.i18n.StringProvider; import de.geeksfactory.opacclient.networking.HttpClientFactory; import de.geeksfactory.opacclient.objects.Account; import de.geeksfactory.opacclient.objects.AccountData; import de.geeksfactory.opacclient.objects.Copy; import de.geeksfactory.opacclient.objects.Detail; import de.geeksfactory.opacclient.objects.DetailedItem; import de.geeksfactory.opacclient.objects.Filter; import de.geeksfactory.opacclient.objects.Filter.Option; import de.geeksfactory.opacclient.objects.LentItem; import de.geeksfactory.opacclient.objects.Library; import de.geeksfactory.opacclient.objects.ReservedItem; import de.geeksfactory.opacclient.objects.SearchRequestResult; import de.geeksfactory.opacclient.objects.SearchResult; import de.geeksfactory.opacclient.objects.SearchResult.Status; import de.geeksfactory.opacclient.searchfields.BarcodeSearchField; import de.geeksfactory.opacclient.searchfields.DropdownSearchField; import de.geeksfactory.opacclient.searchfields.SearchField; import de.geeksfactory.opacclient.searchfields.SearchField.Meaning; import de.geeksfactory.opacclient.searchfields.SearchQuery; import de.geeksfactory.opacclient.searchfields.TextSearchField; import de.geeksfactory.opacclient.utils.Base64; //@formatter:off /** * @author Johan von Forstner, 11.08.2014 * * WinBIAP, Version 4.1.0 gestartet mit Bibliothek Unterföhring * * Unterstützt bisher nur Katalogsuche * * Example for a search query (parameter "data" in the URL, everything before the hyphen, * base64 decoded, added formatting) as seen in Unterföhring: * * cmd=5& perform a search sC= c_0=1%% unknown m_0=1%% * unknown f_0=2%% free * search o_0=8%% contains v_0=schule "schule" ++ c_1=1%% unknown * m_1=1%% unknown * f_1=3%% author o_1=8%% contains v_1=rowling "rowling" ++ * c_2=1%% unknown * m_2=1%% unknown f_2=12%% title o_2=8%% contains * v_2=potter "potter" ++ * c_3=1%% unknown m_3=1%% unknown f_3=34%% year * o_3=6%% newer or equal to * v_3=2000 "2000" ++ c_4=1%% unknown m_4=1%% unknown * f_4=34%% year * o_4=4%% older or equal to v_4=2014 "2014" ++ c_5=1%% unknown * m_5=1%% unknown * f_5=42%% media category o_5=1%% is equal to v_5=3 "Kinder- und * Jugendbücher" ++ * c_6=1%% unknown m_6=1%% unknown f_6=48%% location * o_6=1%% is equal to * v_6=1 "Bibliothek Unterföhring" ++ c_7=3%% unknown (now * changed to 3) - * m_7=1%% unknown | This group has no f_7=45%% * unknown |--- effect on the * result o_7=1%% unknown | and can be left out * v_7=5|4|101|102 unknown - * * &Sort=Autor Sort by Author (default) */ //@formatter:on public class WinBiap extends BaseApi implements OpacApi { protected static final String QUERY_TYPE_CONTAINS = "8"; protected static final String QUERY_TYPE_FROM = "6"; protected static final String QUERY_TYPE_TO = "4"; protected static final String QUERY_TYPE_STARTS_WITH = "7"; protected static final String QUERY_TYPE_EQUALS = "1"; protected static HashMap<String, SearchResult.MediaType> defaulttypes = new HashMap<>(); static { defaulttypes.put("sb_buch", SearchResult.MediaType.BOOK); defaulttypes.put("sl_buch", SearchResult.MediaType.BOOK); defaulttypes.put("kj_buch", SearchResult.MediaType.BOOK); defaulttypes.put("buch", SearchResult.MediaType.BOOK); defaulttypes.put("hoerbuch", SearchResult.MediaType.AUDIOBOOK); defaulttypes.put("musik", SearchResult.MediaType.CD_MUSIC); defaulttypes.put("cdrom", SearchResult.MediaType.CD_SOFTWARE); defaulttypes.put("dvd", SearchResult.MediaType.DVD); defaulttypes.put("online", SearchResult.MediaType.EBOOK); defaulttypes.put("konsole", SearchResult.MediaType.GAME_CONSOLE); defaulttypes.put("zschrift", SearchResult.MediaType.MAGAZINE); } protected String opac_url = ""; protected JSONObject data; protected List<List<NameValuePair>> query; public void init(Library lib, HttpClientFactory httpClientFactory) { super.init(lib, httpClientFactory); this.data = lib.getData(); try { this.opac_url = data.getString("baseurl"); } catch (JSONException e) { throw new RuntimeException(e); } } /** * For documentation of the parameters, @see {@link #addParametersManual(String, String, String, * String, String, List, int)} */ protected int addParameters(Map<String, String> query, String key, String searchkey, String type, List<List<NameValuePair>> params, int index) { if (!query.containsKey(key) || query.get(key).equals("")) { return index; } return addParametersManual("1", "1", searchkey, type, query.get(key), params, index); } /** * @param combination "Combination" (probably And, Or, ...): Meaning unknown, seems to always be * "1" except in some mysterious queries the website adds every time that * don't change the result * @param mode "Mode": Meaning unknown, seems to always be "1" except in some mysterious * queries the website adds every time that don't change the result * @param field "Field": The key for the property that is queried, for example "12" for * "title" * @param operator "Operator": The type of search that is made (one of the QUERY_TYPE_ * constants above), for example "8" for "contains" * @param value "Value": The value that was input by the user */ @SuppressWarnings("SameParameterValue") protected int addParametersManual(String combination, String mode, String field, String operator, String value, List<List<NameValuePair>> params, int index) { List<NameValuePair> list = new ArrayList<>(); if (data.optBoolean("longParameterNames")) { // A few libraries use longer names for the parameters (e.g. Hohen Neuendorf) list.add(new BasicNameValuePair("Combination_" + index, combination)); list.add(new BasicNameValuePair("Mode_" + index, mode)); list.add(new BasicNameValuePair("Searchfield_" + index, field)); list.add(new BasicNameValuePair("Searchoperator_" + index, operator)); list.add(new BasicNameValuePair("Searchvalue_" + index, value)); } else { list.add(new BasicNameValuePair("c_" + index, combination)); list.add(new BasicNameValuePair("m_" + index, mode)); list.add(new BasicNameValuePair("f_" + index, field)); list.add(new BasicNameValuePair("o_" + index, operator)); list.add(new BasicNameValuePair("v_" + index, value)); } params.add(list); return index + 1; } @Override public SearchRequestResult search(List<SearchQuery> queries) throws IOException, OpacErrorException { Map<String, String> query = searchQueryListToMap(queries); List<List<NameValuePair>> queryParams = new ArrayList<>(); int index = 0; index = addParameters(query, KEY_SEARCH_QUERY_FREE, data.optString("KEY_SEARCH_QUERY_FREE", "2"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_AUTHOR, data.optString("KEY_SEARCH_QUERY_AUTHOR", "3"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_TITLE, data.optString("KEY_SEARCH_QUERY_TITLE", "12"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_KEYWORDA, data.optString("KEY_SEARCH_QUERY_KEYWORDA", "24"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_AUDIENCE, data.optString("KEY_SEARCH_QUERY_AUDIENCE", "25"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_SYSTEM, data.optString("KEY_SEARCH_QUERY_SYSTEM", "26"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_ISBN, data.optString("KEY_SEARCH_QUERY_ISBN", "29"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_PUBLISHER, data.optString("KEY_SEARCH_QUERY_PUBLISHER", "32"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_BARCODE, data.optString("KEY_SEARCH_QUERY_BARCODE", "46"), QUERY_TYPE_CONTAINS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_YEAR_RANGE_START, data.optString("KEY_SEARCH_QUERY_BARCODE", "34"), QUERY_TYPE_FROM, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_YEAR_RANGE_END, data.optString("KEY_SEARCH_QUERY_BARCODE", "34"), QUERY_TYPE_TO, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_CATEGORY, data.optString("KEY_SEARCH_QUERY_CATEGORY", "42"), QUERY_TYPE_EQUALS, queryParams, index); index = addParameters(query, KEY_SEARCH_QUERY_BRANCH, data.optString("KEY_SEARCH_QUERY_BRANCH", "48"), QUERY_TYPE_EQUALS, queryParams, index); if (index == 0) { throw new OpacErrorException( stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)); } // if (index > 4) { // throw new OpacErrorException( // "Diese Bibliothek unterstützt nur bis zu vier benutzte Suchkriterien."); // } this.query = queryParams; String encodedQueryParams = encode(queryParams, "=", "%%", "++"); List<NameValuePair> params = new ArrayList<>(); start(); params.add(new BasicNameValuePair("cmd", "5")); if (data.optBoolean("longParameterNames")) // A few libraries use longer names for the parameters // (e.g. Hohen Neuendorf) { params.add(new BasicNameValuePair("searchConditions", encodedQueryParams)); } else { params.add(new BasicNameValuePair("sC", encodedQueryParams)); } params.add(new BasicNameValuePair("Sort", "Autor")); String text = encode(params, "=", "&"); String base64 = URLEncoder.encode( Base64.encodeBytes(text.getBytes("UTF-8")), "UTF-8"); String html = httpGet(opac_url + "/search.aspx?data=" + base64, getDefaultEncoding(), false); return parse_search(html, 1); } private SearchRequestResult parse_search(String html, int page) throws OpacErrorException, IOException { Document doc = Jsoup.parse(html); if (doc.select(".alert h4").text().contains("Keine Treffer gefunden")) { // no media found return new SearchRequestResult(new ArrayList<SearchResult>(), 0, page); } if (doc.select("errortype").size() > 0) { // Error (e.g. 404) throw new OpacErrorException(doc.select("errortype").text()); } // Total count String header = doc.select(".ResultHeader").text(); Pattern pattern = Pattern.compile("Die Suche ergab (\\d*) Treffer"); Matcher matcher = pattern.matcher(header); int results_total; if (matcher.find()) { results_total = Integer.parseInt(matcher.group(1)); } else { throw new OpacErrorException( stringProvider.getString(StringProvider.INTERNAL_ERROR)); } // Results Elements trs = doc.select("#listview .ResultItem"); List<SearchResult> results = new ArrayList<>(); for (Element tr : trs) { SearchResult sr = new SearchResult(); String author = tr.select(".autor").text(); String title = tr.select(".title").text(); String titleAddition = tr.select(".titleZusatz").text(); String desc = tr.select(".smallDescription").text(); sr.setInnerhtml("<b>" + (author.equals("") ? "" : author + "<br />") + title + (titleAddition.equals("") ? "" : " - <i>" + titleAddition + "</i>") + "</b><br /><small>" + desc + "</small>"); if (tr.select(".coverWrapper input, .coverWrapper img").size() > 0) { Element cover = tr.select(".coverWrapper input, .coverWrapper img").first(); if (cover.hasAttr("data-src")) { sr.setCover(cover.attr("data-src")); } else if (cover.hasAttr("src") && !cover.attr("src").contains("empty.gif") && !cover.attr("src").contains("leer.gif")) { sr.setCover(cover.attr("src")); } sr.setType(getMediaType(cover, data)); } String link = tr.select("a[href*=detail.aspx]").attr("href"); String base64 = getQueryParamsFirst(link).get("data"); if (base64.contains("-")) // Most of the time, the base64 string is // followed by a hyphen and some // mysterious // letters that we don't want { base64 = base64.substring(0, base64.indexOf("-") - 1); } String decoded = new String(Base64.decode(base64), "UTF-8"); pattern = Pattern.compile("CatalogueId=(\\d*)"); matcher = pattern.matcher(decoded); if (matcher.find()) { sr.setId(matcher.group(1)); } else { throw new OpacErrorException( stringProvider.getString(StringProvider.INTERNAL_ERROR)); } if (tr.select(".mediaStatus").size() > 0) { Element status = tr.select(".mediaStatus").first(); if (status.hasClass("StatusNotAvailable")) { sr.setStatus(Status.RED); } else if (status.hasClass("StatusAvailable")) { sr.setStatus(Status.GREEN); } else { sr.setStatus(Status.YELLOW); } } else if (tr.select(".showCopies").size() > 0) { // Multiple copies if (tr.nextElementSibling().select(".StatusNotAvailable") .size() == 0) { sr.setStatus(Status.GREEN); } else if (tr.nextElementSibling().select(".StatusAvailable") .size() == 0) { sr.setStatus(Status.RED); } else { sr.setStatus(Status.YELLOW); } } results.add(sr); } return new SearchRequestResult(results, results_total, page); } private String encode(List<List<NameValuePair>> list, String equals, String separator, String separator2) { if (list.size() > 0) { String encoded = encode(list.get(0), equals, separator); for (int i = 1; i < list.size(); i++) { encoded += separator2; encoded += encode(list.get(i), equals, separator); } return encoded; } else { return ""; } } private String encode(List<NameValuePair> list, String equals, String separator) { if (list.size() > 0) { String encoded = list.get(0).getName() + equals + list.get(0).getValue(); for (int i = 1; i < list.size(); i++) { encoded += separator; encoded += list.get(i).getName() + equals + list.get(i).getValue(); } return encoded; } else { return ""; } } @Override public SearchRequestResult filterResults(Filter filter, Option option) throws IOException, OpacErrorException { return null; } @Override public SearchRequestResult searchGetPage(int page) throws IOException, OpacErrorException { String encodedQueryParams = encode(query, "=", "%%", "++"); List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("cmd", "1")); if (data.optBoolean("longParameterNames")) { // A few libraries use longer names for the parameters // (e.g. Hohen Neuendorf) params.add(new BasicNameValuePair("searchConditions", encodedQueryParams)); params.add(new BasicNameValuePair("PageIndex", String .valueOf(page - 1))); } else { params.add(new BasicNameValuePair("sC", encodedQueryParams)); params.add(new BasicNameValuePair("pI", String.valueOf(page - 1))); } params.add(new BasicNameValuePair("Sort", "Autor")); String text = encode(params, "=", "&"); String base64 = URLEncoder.encode( Base64.encodeBytes(text.getBytes("UTF-8")), "UTF-8"); String html = httpGet(opac_url + "/search.aspx?data=" + base64, getDefaultEncoding(), false); return parse_search(html, page); } @Override public DetailedItem getResultById(String id, String homebranch) throws IOException, OpacErrorException { String html = httpGet(opac_url + "/detail.aspx?Id=" + id, getDefaultEncoding(), false); return parse_result(html); } private DetailedItem parse_result(String html) { Document doc = Jsoup.parse(html); DetailedItem item = new DetailedItem(); if (doc.select(".cover").size() > 0) { Element cover = doc.select(".cover").first(); if (cover.hasAttr("data-src")) { item.setCover(cover.attr("data-src")); } else if (cover.hasAttr("src") && !cover.attr("src").equals("images/empty.gif")) { item.setCover(cover.attr("src")); } item.setMediaType(getMediaType(cover, data)); } String permalink = doc.select(".PermalinkTextarea").text(); item.setId(getQueryParamsFirst(permalink).get("Id")); Elements trs = doc.select(".DetailInformation").first().select("tr"); for (Element tr : trs) { String name = tr.select(".DetailInformationEntryName").text() .replace(":", ""); String value = tr.select(".DetailInformationEntryContent").text(); switch (name) { case "Titel": item.setTitle(value); break; case "Stücktitel": item.setTitle(item.getTitle() + " " + value); break; default: item.addDetail(new Detail(name, value)); break; } } DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); trs = doc.select(".detailCopies .tableCopies > tbody > tr:not(.headerCopies)"); for (Element tr : trs) { Copy copy = new Copy(); copy.setBarcode(tr.select(".mediaBarcode").text().replace("#", "")); copy.setStatus(tr.select(".mediaStatus").text()); if (tr.select(".DateofReturn .borrowUntil").size() > 0) { String returntime = tr.select(".DateofReturn .borrowUntil").text(); try { copy.setReturnDate(fmt.parseLocalDate(returntime)); } catch (IllegalArgumentException e) { e.printStackTrace(); } } if (tr.select(".mediaBranch").size() > 0) { copy.setBranch(tr.select(".mediaBranch").text()); } copy.setLocation(tr.select(".cellMediaItemLocation span").text()); if (tr.select("#HyperLinkReservation").size() > 0) { copy.setResInfo(tr.select("#HyperLinkReservation").attr("href")); item.setReservable(true); item.setReservation_info("reservable"); } item.addCopy(copy); } return item; } private static SearchResult.MediaType getMediaType(Element cover, JSONObject data) { if (cover.hasAttr("grp")) { String[] parts = cover.attr("grp").split("/"); String fname = parts[parts.length - 1]; if (data.has("mediatypes")) { try { return SearchResult.MediaType.valueOf(data.getJSONObject( "mediatypes").getString(fname)); } catch (JSONException | IllegalArgumentException e) { return defaulttypes.get(fname .toLowerCase(Locale.GERMAN).replace(".jpg", "") .replace(".gif", "").replace(".png", "")); } } else { return defaulttypes.get(fname .toLowerCase(Locale.GERMAN).replace(".jpg", "") .replace(".gif", "").replace(".png", "")); } } return null; } @Override public DetailedItem getResult(int position) throws IOException, OpacErrorException { // Should not be called because every media has an ID return null; } @Override public ReservationResult reservation(DetailedItem item, Account account, int useraction, String selection) throws IOException { if (selection == null) { // Which copy? List<Map<String, String>> options = new ArrayList<>(); for (Copy copy : item.getCopies()) { if (copy.getResInfo() == null) continue; Map<String, String> option = new HashMap<>(); option.put("key", copy.getResInfo()); option.put("value", copy.getBarcode() + " - " + copy.getBranch() + " - " + copy.getReturnDate()); options.add(option); } if (options.size() == 0) { return new ReservationResult(MultiStepResult.Status.ERROR, stringProvider.getString(StringProvider.NO_COPY_RESERVABLE)); } else if (options.size() == 1) { return reservation(item, account, useraction, options.get(0).get("key")); } else { ReservationResult res = new ReservationResult( MultiStepResult.Status.SELECTION_NEEDED); res.setSelection(options); return res; } } else { // Reservation // the URL stored in selection might be absolute (WinBiap 4.3) or relative (4.2) String reservationUrl = new URL(new URL(opac_url), selection).toString(); // the URL stored in selection contains "=" and other things inside params // and will be messed up by our cleanUrl function, therefore we use a direct HttpGet Document doc = Jsoup.parse(convertStreamToString( http_client.execute(new HttpGet( reservationUrl)) .getEntity().getContent())); if (doc.select("[id$=LabelLoginMessage]").size() > 0) { doc.select("[id$=TextBoxLoginName]").val(account.getName()); doc.select("[id$=TextBoxLoginPassword]").val(account.getPassword()); FormElement form = (FormElement) doc.select("form").first(); List<Connection.KeyVal> formData = form.formData(); List<NameValuePair> params = new ArrayList<>(); for (Connection.KeyVal kv : formData) { if (!kv.key().contains("Button") || kv.key().endsWith("ButtonLogin")) { params.add(new BasicNameValuePair(kv.key(), kv.value())); } } doc = Jsoup.parse(httpPost(opac_url + "/user/" + form.attr("action"), new UrlEncodedFormEntity(params), getDefaultEncoding())); } FormElement confirmationForm = (FormElement) doc.select("form").first(); List<Connection.KeyVal> formData = confirmationForm.formData(); List<NameValuePair> params = new ArrayList<>(); for (Connection.KeyVal kv : formData) { if (!kv.key().contains("Button") || kv.key().endsWith("ButtonVorbestOk")) { params.add(new BasicNameValuePair(kv.key(), kv.value())); } } httpPost(opac_url + "/user/" + confirmationForm.attr("action"), new UrlEncodedFormEntity(params), getDefaultEncoding()); // TODO: handle errors (I did not encounter any) return new ReservationResult(MultiStepResult.Status.OK); } } @Override public ProlongResult prolong(String media, Account account, int useraction, String selection) throws IOException { try { login(account); } catch (OpacErrorException e) { return new ProlongResult(MultiStepResult.Status.ERROR, e.getMessage()); } Document lentPage = Jsoup.parse( httpGet(opac_url + "/user/borrow.aspx", getDefaultEncoding())); lentPage.select("input[name=" + media + "]").first().attr("checked", true); List<Connection.KeyVal> formData = ((FormElement) lentPage.select("form").first()).formData(); List<NameValuePair> params = new ArrayList<>(); for (Connection.KeyVal kv : formData) { params.add(new BasicNameValuePair(kv.key(), kv.value())); } if (lentPage.select("a[id$=ButtonBorrowChecked][href^=javascript]").size() > 0) { String href = lentPage.select("a[id$=ButtonBorrowChecked][href^=javascript]").attr("href"); Pattern pattern = Pattern.compile("javascript:__doPostBack\\('([^,]*)','([^\\)]*)'\\)"); Matcher matcher = pattern.matcher(href); if (!matcher.find()) { return new ProlongResult(MultiStepResult.Status.ERROR, StringProvider.INTERNAL_ERROR); } params.add(new BasicNameValuePair("__EVENTTARGET", matcher.group(1))); params.add(new BasicNameValuePair("__EVENTARGUMENT", matcher.group(2))); } String html = httpPost(opac_url + "/user/borrow.aspx", new UrlEncodedFormEntity (params), getDefaultEncoding()); Document confirmationPage = Jsoup.parse(html); FormElement confirmationForm = (FormElement) confirmationPage.select("form").first(); List<Connection.KeyVal> formData2 = confirmationForm.formData(); List<NameValuePair> params2 = new ArrayList<>(); for (Connection.KeyVal kv : formData2) { if (!kv.key().contains("Button") || kv.key().endsWith("ButtonProlongationOk")) { params2.add(new BasicNameValuePair(kv.key(), kv.value())); } } httpPost(opac_url + "/user/" + confirmationForm.attr("action"), new UrlEncodedFormEntity(params2), getDefaultEncoding()); // TODO: handle errors (I did not encounter any) return new ProlongResult(MultiStepResult.Status.OK); } @Override public ProlongAllResult prolongAll(Account account, int useraction, String selection) throws IOException { return null; } @Override public CancelResult cancel(String media, Account account, int useraction, String selection) throws IOException, OpacErrorException { try { login(account); } catch (OpacErrorException e) { return new CancelResult(MultiStepResult.Status.ERROR, e.getMessage()); } List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("action", "reservationdelete")); params.add(new BasicNameValuePair("data", media)); String response = httpPost(opac_url + "/service/UserService.ashx", new UrlEncodedFormEntity(params), getDefaultEncoding()); if (response.startsWith("{")) { // new system (starting with 4.4.0?): JSON response // e.g. {"Success":true,"Count":0} (Count = number of remaining reservations) try { JSONObject responseJson = new JSONObject(response); if (responseJson.optBoolean("Success")) { return new CancelResult(MultiStepResult.Status.OK); } else { return new CancelResult(MultiStepResult.Status.ERROR, stringProvider.getString(StringProvider.UNKNOWN_ERROR)); } } catch (JSONException e) { return new CancelResult(MultiStepResult.Status.ERROR, stringProvider.getString(StringProvider.INTERNAL_ERROR)); } } else { // Old system // Response: [number of reservations deleted];[number of remaining reservations] String[] parts = response.split(";"); if (parts[0].equals("1")) { return new CancelResult(MultiStepResult.Status.OK); } else { return new CancelResult(MultiStepResult.Status.ERROR, stringProvider.getString(StringProvider.UNKNOWN_ERROR)); } } } @Override public AccountData account(Account account) throws IOException, JSONException, OpacErrorException { Document startPage = login(account); AccountData adata = new AccountData(account.getId()); if (startPage.select("#ctl00_ContentPlaceHolderMain_LabelCharges").size() > 0) { String fees = startPage.select("#ctl00_ContentPlaceHolderMain_LabelCharges").text() .replace("Kontostand:", "").trim(); if (!fees.equals("ausgeglichen")) adata.setPendingFees(fees); } String lentUrl = opac_url + "/user/borrow.aspx"; Document lentPage = Jsoup.parse(httpGet(lentUrl, getDefaultEncoding())); lentPage.setBaseUri(lentUrl); adata.setLent(parseMediaList(lentPage, data)); String resUrl = opac_url + "/user/reservations.aspx"; Document reservationsPage = Jsoup.parse(httpGet(resUrl, getDefaultEncoding())); reservationsPage.setBaseUri(resUrl); adata.setReservations(parseResList(reservationsPage, stringProvider, data)); return adata; } static List<LentItem> parseMediaList(Document doc, JSONObject data) { List<LentItem> lent = new ArrayList<>(); DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); // the account page differs between WinBiap versions 4.2 and >= 4.3 boolean winBiap43; if (doc.select(".GridView_RowStyle").size() > 0) { winBiap43 = false; } else { winBiap43 = true; } // 4.2: .GridView_RowStyle // 4.3: id=...DetailItemMain_rowBorrow // 4.4: id=...DetailItemMain_0_rowBorrow_0 for (Element tr : doc.select(winBiap43 ? ".detailTable tr[id*=_rowBorrow]" : ".GridView_RowStyle")) { LentItem item = new LentItem(); Element detailsTr = winBiap43 ? tr.nextElementSibling() : tr; // the second column contains an img tag with the cover if (detailsTr.select(".cover, img[id*=ImageCover]").size() > 0) { // find media ID using cover URL Element cover = detailsTr.select(".cover, img[id*=ImageCover]").first(); String src = cover.attr("abs:data-src"); if (src.equals("")) src = cover.attr("abs:src"); Map<String, String> params = getQueryParamsFirst(src); if (params.containsKey("catid")) item.setId(params.get("catid")); // find media type SearchResult.MediaType mt = getMediaType(cover, data); item.setMediaType(mt); // set cover if it's not the media type image if (!src.equals(cover.attr("grp"))) item.setCover(src); } item.setAuthor(nullIfEmpty(tr.select("[id$=LabelAutor]").text())); item.setTitle( nullIfEmpty(tr.select("[id$=LabelTitel], [id$=LabelTitle], .title").text())); item.setBarcode(nullIfEmpty(detailsTr .select("[id$=Label_Mediennr], [id$=labelMediennr], [id*=labelMediennr_]") .text())); item.setFormat(nullIfEmpty(detailsTr .select("[id$=Label_Mediengruppe], [id$=labelMediagroup], " + "[id*=labelMediagroup_]") .text())); item.setHomeBranch(nullIfEmpty(detailsTr .select("[id$=Label_Zweigstelle], [id$=labelBranch], [id*=labelBranch_]") .text())); // Label_Entliehen contains the date when the medium was lent try { item.setDeadline(fmt.parseLocalDate(tr.select("[id$=LabelFaellig], [id$=LabelMatureDate], .matureDate").text())); } catch (IllegalArgumentException e) { e.printStackTrace(); } if (tr.select("input[id*=_chkSelect]").size() > 0) { item.setProlongData(tr.select("input[id*=_chkSelect]").attr("name")); } else { item.setRenewable(false); } lent.add(item); } return lent; } private static String nullIfEmpty(String text) { if (text == null || text.equals("")) { return null; } else { return text; } } static List<ReservedItem> parseResList(Document doc, StringProvider stringProvider, JSONObject data) { List<ReservedItem> reservations = new ArrayList<>(); // the account page differs between WinBiap versions 4.2 and 4.3 boolean winBiap43; if (doc.select("tr[id*=GridViewReservation]").size() > 0) { winBiap43 = false; } else { winBiap43 = true; } // 4.2: id=...GridViewReservation_ct... // 4.3: id=...DetailItemMain_rowBorrow // 4.4: id=...DetailItemMain_0_rowBorrow_0 for (Element tr : doc .select(winBiap43 ? ".detailTable tr[id*=_rowBorrow]" : "tr[id*=GridViewReservation]")) { ReservedItem item = new ReservedItem(); Element detailsTr = winBiap43 ? tr.nextElementSibling() : tr; // the second column contains an img tag with the cover if (detailsTr.select(".cover, img[id*=ImageCover]").size() > 0) { // find media ID using cover URL Element cover = detailsTr.select(".cover, img[id*=ImageCover]").first(); String src = cover.attr("abs:data-src"); if (src.equals("")) src = cover.attr("abs:src"); Map<String, String> params = getQueryParamsFirst(src); if (params.containsKey("catid")) item.setId(params.get("catid")); // find media type SearchResult.MediaType mt = getMediaType(cover, data); if (mt != null) { item.setFormat(stringProvider.getMediaTypeName(mt)); item.setMediaType(mt); } // set cover if it's not the media type image if (!src.equals(cover.attr("grp"))) item.setCover(src); } item.setStatus(nullIfEmpty(winBiap43 ? detailsTr.select("[id$=labelStatus], [id*=labelStatus_]").text() : tr.select("[id$=ImageBorrow]").attr("title"))); item.setAuthor(nullIfEmpty(tr.select("[id$=LabelAutor], .autor").text())); item.setTitle(nullIfEmpty(tr.select("[id$=LabelTitle], .title").text())); item.setBranch(nullIfEmpty( detailsTr.select("[id$=LabelBranch], [id$=labelBranch], [id*=labelBranch_]") .text())); item.setFormat(nullIfEmpty(detailsTr .select("[id$=Label_Mediengruppe], [id$=labelMediagroup], [id*=labelMediagroup_]") .text())); // Label_Vorbestelltam contains the date when the medium was reserved if (tr.select("a[id$=ImageReservationDelete]").size() > 0) { String javascript = tr.select("a[id$=ImageReservationDelete]").attr("onclick"); /* Javascript example: javascript:DeleteReservation( '#ctl00_ContentPlaceHolderMain_GridViewReservation_ctl02', '#ctl00_ContentPlaceHolderMain_GridViewReservation_ctl02_ImageReservationDelete', 'cmVzZXJ2YXRpb25JZD00MDk1JmFtcDtyZWFkZXJJZD05MzIwJmFtcDttb2RlPTE=-f2yu2300+t4=', '../service/UserService.ashx', 'Vorbestellung: \'Beck, Rufus - Harry Potter Folge 4. Harry Potter und der Feuerkelch\' wirklich löschen?', '#ctl00_ContentPlaceHolderMain_LabelAccountTableResult', 'Sie haben derzeit $ Medien vorbestellt!'); return false; We need the 3rd parameter (Base64 string) and will find it using the following massive RegEx. */ Pattern regex = Pattern.compile("javascript:DeleteReservation\\('" + "(?:\\\\[\\\\']|[^\\\\'])*'\\s*,\\s*'(?:\\\\[\\\\']|[^\\\\'])*'\\s*,\\s*'" + "((?:\\\\[\\\\']|[^\\\\'])*)'\\s*,\\s*'(?:\\\\[\\\\']|[^\\\\'])*'\\s*," + "\\s*'(?:\\\\[\\\\']|[^\\\\'])*'\\s*,\\s*'(?:\\\\[\\\\']|[^\\\\'])*'\\s*," + "\\s*'(?:\\\\[\\\\']|[^\\\\'])*'\\s*\\);"); Matcher matcher = regex.matcher(javascript); if (matcher.find()) { String base64 = matcher.group(1); item.setCancelData(base64); } } else if (detailsTr.select("input[id*=_hiddenValueDetail][value]").size() > 0) { item.setCancelData( detailsTr.select("input[id*=_hiddenValueDetail][value]").attr("value")); } reservations.add(item); } return reservations; } @Override public List<SearchField> parseSearchFields() throws IOException { // extract branches and categories String html = httpGet(opac_url + "/search.aspx", getDefaultEncoding()); Document doc = Jsoup.parse(html); Elements mediaGroupOptions = doc .select("[id$=ListBoxMediagroups_ListBoxMultiselection] option"); Elements branchOptions = doc .select("[id$=MultiSelectionBranch_ListBoxMultiselection] option"); final DropdownSearchField mediaGroups = new DropdownSearchField(KEY_SEARCH_QUERY_CATEGORY, "Mediengruppe", false, null); mediaGroups.setMeaning(Meaning.CATEGORY); final DropdownSearchField branches = new DropdownSearchField(KEY_SEARCH_QUERY_BRANCH, "Zweigstelle", false, null); branches.setMeaning(Meaning.BRANCH); mediaGroups.addDropdownValue("", "Alle"); branches.addDropdownValue("", "Alle"); for (Element option : mediaGroupOptions) { mediaGroups.addDropdownValue(option.attr("value"), option.text()); } for (Element option : branchOptions) { branches.addDropdownValue(option.attr("value"), option.text()); } List<SearchField> searchFields = new ArrayList<>(); SearchField field = new TextSearchField(KEY_SEARCH_QUERY_FREE, "", false, false, "Beliebig", true, false); field.setMeaning(Meaning.FREE); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_AUTHOR, "Autor", false, false, "Nachname, Vorname", false, false); field.setMeaning(Meaning.AUTHOR); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_TITLE, "Titel", false, false, "Stichwort", false, false); field.setMeaning(Meaning.TITLE); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_KEYWORDA, "Schlagwort", true, false, "", false, false); field.setMeaning(Meaning.KEYWORD); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_AUDIENCE, "Interessenkreis", true, false, "", false, false); field.setMeaning(Meaning.AUDIENCE); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_SYSTEM, "Systematik", true, false, "", false, false); field.setMeaning(Meaning.SYSTEM); searchFields.add(field); field = new BarcodeSearchField(KEY_SEARCH_QUERY_ISBN, "Strichcode", false, false, "ISBN"); field.setMeaning(Meaning.ISBN); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_PUBLISHER, "Verlag", false, false, "", false, false); field.setMeaning(Meaning.PUBLISHER); searchFields.add(field); field = new BarcodeSearchField(KEY_SEARCH_QUERY_BARCODE, "Strichcode", false, true, "Mediennummer"); field.setMeaning(Meaning.BARCODE); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_YEAR_RANGE_START, "Jahr", false, false, "von", false, true); field.setMeaning(Meaning.YEAR); searchFields.add(field); field = new TextSearchField(KEY_SEARCH_QUERY_YEAR_RANGE_END, "Jahr", false, true, "bis", false, true); field.setMeaning(Meaning.YEAR); searchFields.add(field); searchFields.add(branches); searchFields.add(mediaGroups); return searchFields; } @Override public boolean shouldUseMeaningDetector() { return false; } @Override public String getShareUrl(String id, String title) { return opac_url + "/detail.aspx?Id=" + id; } @Override public int getSupportFlags() { return SUPPORT_FLAG_ENDLESS_SCROLLING; } @Override public void checkAccountData(Account account) throws IOException, JSONException, OpacErrorException { login(account); } protected Document login(Account account) throws IOException, OpacErrorException { Document loginPage = Jsoup.parse( httpGet(opac_url + "/user/login.aspx", getDefaultEncoding())); List<NameValuePair> data = new ArrayList<>(); String formAction = loginPage.select("form").attr("action"); boolean homePage = formAction.endsWith("index.aspx"); /* pass all input fields beginning with two underscores to login url */ Elements inputFields = loginPage.select("input[id^=__]"); for (Element inputField : inputFields) { data.add(new BasicNameValuePair(inputField.attr("name"), inputField.val())); } // Some WinBiap 4.4 installations (such as Neufahrn) redirect user/login.aspx to index.aspx // This page then also has a login form, but it has different text box IDs: // TextBoxLoginName -> TextBoxUsername and TextBoxLoginPassword -> TextBoxPassword data.add(new BasicNameValuePair( loginPage.select("input[id$=TextBoxLoginName], input[id$=TextBoxUsername]") .attr("name"), account.getName())); data.add(new BasicNameValuePair( loginPage.select("input[id$=TextBoxLoginPassword], input[id$=TextBoxPassword]") .attr("name"), account.getPassword())); data.add(new BasicNameValuePair(loginPage.select("input[id$=ButtonLogin]").attr("name"), "Anmelden")); // We also need to POST our data to the correct page. String postUrl = opac_url + (homePage ? "/index.aspx" : "/user/login.aspx"); String html = httpPost(postUrl, new UrlEncodedFormEntity(data), "UTF-8"); Document doc = Jsoup.parse(html); if (doc.select("#ctl00_ContentPlaceHolderMain_LabelLoginMessage").size() > 0) { throw new OpacErrorException( doc.select("#ctl00_ContentPlaceHolderMain_LabelLoginMessage").text()); } return doc; } @Override public void setLanguage(String language) { // TODO Auto-generated method stub } @Override public Set<String> getSupportedLanguages() throws IOException { // TODO Auto-generated method stub return null; } }