/* * Copyright (C) 2015 by Johan von Forstner 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 org.apache.http.HttpEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.json.JSONException; import org.json.JSONObject; 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.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; 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.networking.NotReachableException; 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.Library; import de.geeksfactory.opacclient.objects.SearchRequestResult; import de.geeksfactory.opacclient.objects.SearchResult; import de.geeksfactory.opacclient.searchfields.BarcodeSearchField; import de.geeksfactory.opacclient.searchfields.CheckboxSearchField; import de.geeksfactory.opacclient.searchfields.DropdownSearchField; import de.geeksfactory.opacclient.searchfields.SearchField; import de.geeksfactory.opacclient.searchfields.SearchQuery; import de.geeksfactory.opacclient.searchfields.TextSearchField; /** * API for Bibliotheca+/OPEN OPAC software * * @author Johan von Forstner, 29.03.2015 */ public class Open extends BaseApi implements OpacApi { protected JSONObject data; protected String opac_url; protected Document searchResultDoc; protected static HashMap<String, SearchResult.MediaType> defaulttypes = new HashMap<>(); static { defaulttypes.put("archv", SearchResult.MediaType.BOOK); defaulttypes.put("archv-digital", SearchResult.MediaType.EDOC); defaulttypes.put("artchap", SearchResult.MediaType.ART); defaulttypes.put("artchap-artcl", SearchResult.MediaType.ART); defaulttypes.put("artchap-chptr", SearchResult.MediaType.ART); defaulttypes.put("artchap-digital", SearchResult.MediaType.ART); defaulttypes.put("audiobook", SearchResult.MediaType.AUDIOBOOK); defaulttypes.put("audiobook-cd", SearchResult.MediaType.AUDIOBOOK); defaulttypes.put("audiobook-lp", SearchResult.MediaType.AUDIOBOOK); defaulttypes.put("audiobook-digital", SearchResult.MediaType.MP3); defaulttypes.put("book", SearchResult.MediaType.BOOK); defaulttypes.put("book-braille", SearchResult.MediaType.BOOK); defaulttypes.put("book-continuing", SearchResult.MediaType.BOOK); defaulttypes.put("book-digital", SearchResult.MediaType.EBOOK); defaulttypes.put("book-largeprint", SearchResult.MediaType.BOOK); defaulttypes.put("book-mic", SearchResult.MediaType.BOOK); defaulttypes.put("book-thsis", SearchResult.MediaType.BOOK); defaulttypes.put("compfile", SearchResult.MediaType.PACKAGE); defaulttypes.put("compfile-digital", SearchResult.MediaType.PACKAGE); defaulttypes.put("corpprof", SearchResult.MediaType.UNKNOWN); defaulttypes.put("encyc", SearchResult.MediaType.UNKNOWN); defaulttypes.put("game", SearchResult.MediaType.BOARDGAME); defaulttypes.put("game-digital", SearchResult.MediaType.GAME_CONSOLE); defaulttypes.put("image", SearchResult.MediaType.ART); defaulttypes.put("image-2d", SearchResult.MediaType.ART); defaulttypes.put("intmm", SearchResult.MediaType.EVIDEO); defaulttypes.put("intmm-digital", SearchResult.MediaType.EVIDEO); defaulttypes.put("jrnl", SearchResult.MediaType.MAGAZINE); defaulttypes.put("jrnl-issue", SearchResult.MediaType.MAGAZINE); defaulttypes.put("jrnl-digital", SearchResult.MediaType.EBOOK); defaulttypes.put("kit", SearchResult.MediaType.PACKAGE); defaulttypes.put("map", SearchResult.MediaType.MAP); defaulttypes.put("map-digital", SearchResult.MediaType.EBOOK); defaulttypes.put("msscr", SearchResult.MediaType.MP3); defaulttypes.put("msscr-digital", SearchResult.MediaType.MP3); defaulttypes.put("music", SearchResult.MediaType.MP3); defaulttypes.put("music-cassette", SearchResult.MediaType.AUDIO_CASSETTE); defaulttypes.put("music-cd", SearchResult.MediaType.CD_MUSIC); defaulttypes.put("music-digital", SearchResult.MediaType.MP3); defaulttypes.put("music-lp", SearchResult.MediaType.LP_RECORD); defaulttypes.put("news", SearchResult.MediaType.NEWSPAPER); defaulttypes.put("news-digital", SearchResult.MediaType.EBOOK); defaulttypes.put("object", SearchResult.MediaType.UNKNOWN); defaulttypes.put("object-digital", SearchResult.MediaType.UNKNOWN); defaulttypes.put("paper", SearchResult.MediaType.UNKNOWN); defaulttypes.put("pub", SearchResult.MediaType.UNKNOWN); defaulttypes.put("rev", SearchResult.MediaType.UNKNOWN); defaulttypes.put("snd", SearchResult.MediaType.MP3); defaulttypes.put("snd-cassette", SearchResult.MediaType.AUDIO_CASSETTE); defaulttypes.put("snd-cd", SearchResult.MediaType.CD_MUSIC); defaulttypes.put("snd-lp", SearchResult.MediaType.LP_RECORD); defaulttypes.put("snd-digital", SearchResult.MediaType.EAUDIO); defaulttypes.put("toy", SearchResult.MediaType.BOARDGAME); defaulttypes.put("und", SearchResult.MediaType.UNKNOWN); defaulttypes.put("video-bluray", SearchResult.MediaType.BLURAY); defaulttypes.put("video-digital", SearchResult.MediaType.EVIDEO); defaulttypes.put("video-dvd", SearchResult.MediaType.DVD); defaulttypes.put("video-film", SearchResult.MediaType.MOVIE); defaulttypes.put("video-vhs", SearchResult.MediaType.MOVIE); defaulttypes.put("vis", SearchResult.MediaType.ART); defaulttypes.put("vis-digital", SearchResult.MediaType.ART); defaulttypes.put("web", SearchResult.MediaType.URL); defaulttypes.put("web-digital", SearchResult.MediaType.URL); defaulttypes.put("art", SearchResult.MediaType.ART); defaulttypes.put("arturl", SearchResult.MediaType.URL); defaulttypes.put("bks", SearchResult.MediaType.BOOK); defaulttypes.put("bksbrl", SearchResult.MediaType.BOOK); defaulttypes.put("bksdeg", SearchResult.MediaType.BOOK); defaulttypes.put("bkslpt", SearchResult.MediaType.BOOK); defaulttypes.put("bksurl", SearchResult.MediaType.EBOOK); defaulttypes.put("braille", SearchResult.MediaType.BOOK); defaulttypes.put("com", SearchResult.MediaType.CD_SOFTWARE); defaulttypes.put("comcgm", SearchResult.MediaType.GAME_CONSOLE); defaulttypes.put("comcgmurl", SearchResult.MediaType.GAME_CONSOLE); defaulttypes.put("comimm", SearchResult.MediaType.EVIDEO); defaulttypes.put("comimmurl", SearchResult.MediaType.EVIDEO); defaulttypes.put("comurl", SearchResult.MediaType.URL); defaulttypes.put("int", SearchResult.MediaType.UNKNOWN); defaulttypes.put("inturl", SearchResult.MediaType.UNKNOWN); defaulttypes.put("map", SearchResult.MediaType.MAP); defaulttypes.put("mapurl", SearchResult.MediaType.MAP); defaulttypes.put("mic", SearchResult.MediaType.UNKNOWN); defaulttypes.put("micro", SearchResult.MediaType.UNKNOWN); defaulttypes.put("mix", SearchResult.MediaType.PACKAGE); defaulttypes.put("mixurl", SearchResult.MediaType.PACKAGE); defaulttypes.put("rec", SearchResult.MediaType.MP3); defaulttypes.put("recmsr", SearchResult.MediaType.MP3); defaulttypes.put("recmsrcas", SearchResult.MediaType.AUDIO_CASSETTE); defaulttypes.put("recmsrcda", SearchResult.MediaType.CD_MUSIC); defaulttypes.put("recmsrlps", SearchResult.MediaType.LP_RECORD); defaulttypes.put("recmsrurl", SearchResult.MediaType.EAUDIO); defaulttypes.put("recnsr", SearchResult.MediaType.UNKNOWN); defaulttypes.put("recnsrcas", SearchResult.MediaType.UNKNOWN); defaulttypes.put("recnsrcda", SearchResult.MediaType.UNKNOWN); defaulttypes.put("recnsrlps", SearchResult.MediaType.UNKNOWN); defaulttypes.put("recnsrurl", SearchResult.MediaType.UNKNOWN); defaulttypes.put("recurl", SearchResult.MediaType.EAUDIO); defaulttypes.put("sco", SearchResult.MediaType.SCORE_MUSIC); defaulttypes.put("scourl", SearchResult.MediaType.SCORE_MUSIC); defaulttypes.put("ser", SearchResult.MediaType.PACKAGE_BOOKS); defaulttypes.put("sernew", SearchResult.MediaType.PACKAGE_BOOKS); defaulttypes.put("sernewurl", SearchResult.MediaType.PACKAGE_BOOKS); defaulttypes.put("serurl", SearchResult.MediaType.PACKAGE_BOOKS); defaulttypes.put("url", SearchResult.MediaType.URL); defaulttypes.put("vis", SearchResult.MediaType.ART); defaulttypes.put("visart", SearchResult.MediaType.ART); defaulttypes.put("visdvv", SearchResult.MediaType.DVD); defaulttypes.put("vismot", SearchResult.MediaType.MOVIE); defaulttypes.put("visngr", SearchResult.MediaType.ART); defaulttypes.put("visngrurl", SearchResult.MediaType.ART); defaulttypes.put("visphg", SearchResult.MediaType.BOARDGAME); defaulttypes.put("vistoy", SearchResult.MediaType.BOARDGAME); defaulttypes.put("visurl", SearchResult.MediaType.URL); defaulttypes.put("visvhs", SearchResult.MediaType.MOVIE); defaulttypes.put("visvid", SearchResult.MediaType.MOVIE); defaulttypes.put("visvidurl", SearchResult.MediaType.EVIDEO); defaulttypes.put("web", SearchResult.MediaType.URL); } /** * This parameter needs to be passed to a URL to make sure we are not redirected to the mobile * site */ protected static final String NO_MOBILE = "?nomo=1"; @Override 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); } } @Override public SearchRequestResult search(List<SearchQuery> queries) throws IOException, OpacErrorException, JSONException { String url = opac_url + "/" + data.getJSONObject("urls").getString("advanced_search") + NO_MOBILE; Document doc = Jsoup.parse(httpGet(url, getDefaultEncoding())); doc.setBaseUri(url); int selectableCount = 0; for (SearchQuery query : queries) { if (query.getValue().equals("") || query.getValue().equals("false")) continue; if (query.getSearchField() instanceof TextSearchField | query.getSearchField() instanceof BarcodeSearchField) { SearchField field = query.getSearchField(); if (field.getData().getBoolean("selectable")) { selectableCount++; if (selectableCount > 3) { throw new OpacErrorException(stringProvider.getQuantityString( StringProvider.LIMITED_NUM_OF_CRITERIA, 3, 3)); } String number = numberToText(selectableCount); Element searchField = doc.select("select[name$=" + number + "SearchField]").first(); Element searchValue = doc.select("input[name$=" + number + "SearchValue]").first(); setSelectValue(searchField, field.getId()); searchValue.val(query.getValue()); } else { Element input = doc.select("input[name=" + field.getId() + "]").first(); input.val(query.getValue()); } } else if (query.getSearchField() instanceof DropdownSearchField) { DropdownSearchField field = (DropdownSearchField) query.getSearchField(); Element select = doc.select("select[name=" + field.getId() + "]").first(); setSelectValue(select, query.getValue()); } else if (query.getSearchField() instanceof CheckboxSearchField) { CheckboxSearchField field = (CheckboxSearchField) query.getSearchField(); Element input = doc.select("input[name=" + field.getId() + "]").first(); input.attr("checked", query.getValue()); } } // Submit form FormElement form = (FormElement) doc.select("form").first(); HttpEntity data = formData(form, "BtnSearch").build(); String postUrl = form.attr("abs:action"); String html = httpPost(postUrl, data, "UTF-8"); Document doc2 = Jsoup.parse(html); doc2.setBaseUri(postUrl); return parse_search(doc2, 0); } protected void setSelectValue(Element select, String value) { for (Element opt : select.select("option")) { if (value.equals(opt.val())) { opt.attr("selected", "selected"); } else { opt.removeAttr("selected"); } } } protected SearchRequestResult parse_search(Document doc, int page) throws OpacErrorException { searchResultDoc = doc; if (doc.select("#Label1, span[id$=LblInfoMessage]").size() > 0) { String message = doc.select("#Label1, span[id$=LblInfoMessage]").text(); if (message.contains("keine Treffer")) { return new SearchRequestResult(new ArrayList<SearchResult>(), 0, 1, page); } else { throw new OpacErrorException(message); } } int totalCount; if (doc.select("span[id$=TotalItemsLabel]").size() > 0) { totalCount = Integer.parseInt(doc.select("span[id$=TotalItemsLabel]").first().text()); } else { throw new OpacErrorException(stringProvider.getString(StringProvider.UNKNOWN_ERROR)); } Pattern idPattern = Pattern.compile("\\$(mdv|civ|dcv)(\\d+)\\$"); Pattern weakIdPattern = Pattern.compile("(mdv|civ|dcv)(\\d+)[^\\d]"); Elements elements = doc.select("div[id$=divMedium], div[id$=divComprehensiveItem]"); List<SearchResult> results = new ArrayList<>(); int i = 0; for (Element element : elements) { SearchResult result = new SearchResult(); // Cover if (element.select("input[id$=mediumImage]").size() > 0) { result.setCover(element.select("input[id$=mediumImage]").first().attr("src")); } else if (element.select("img[id$=CoverView_Image]").size() > 0) { result.setCover(getCoverUrl(element.select("img[id$=CoverView_Image]").first())); } Element catalogueContent = element.select(".catalogueContent").first(); // Media Type if (catalogueContent.select("#spanMediaGrpIcon").size() > 0) { String mediatype = catalogueContent.select("#spanMediaGrpIcon").attr("class"); if (mediatype.startsWith("itemtype ")) { mediatype = mediatype.substring("itemtype ".length()); } SearchResult.MediaType defaulttype = defaulttypes.get(mediatype); if (defaulttype == null) defaulttype = SearchResult.MediaType.UNKNOWN; if (data.has("mediatypes")) { try { result.setType(SearchResult.MediaType .valueOf(data.getJSONObject("mediatypes").getString(mediatype))); } catch (JSONException e) { result.setType(defaulttype); } } else { result.setType(defaulttype); } } else { result.setType(SearchResult.MediaType.UNKNOWN); } // Text String title = catalogueContent .select("a[id$=LbtnShortDescriptionValue], a[id$=LbtnTitleValue]").text(); String subtitle = catalogueContent.select("span[id$=LblSubTitleValue]").text(); String author = catalogueContent.select("span[id$=LblAuthorValue]").text(); String year = catalogueContent.select("span[id$=LblProductionYearValue]").text(); String series = catalogueContent.select("span[id$=LblSeriesValue]").text(); // Some libraries, such as Bern, have labels but no <span id="..Value"> tags int j = 0; for (Element div : catalogueContent.children()) { if (subtitle.equals("") && div.select("span").size() == 0 && j > 0 && j < 3) { subtitle = div.text().trim(); } if (author.equals("") && div.select("span[id$=LblAuthor]").size() == 1) { author = div.text().trim(); if (author.contains(":")) { author = author.split(":")[1]; } } if (year.equals("") && div.select("span[id$=LblProductionYear]").size() == 1) { year = div.text().trim(); if (year.contains(":")) { year = year.split(":")[1]; } } j++; } StringBuilder text = new StringBuilder(); text.append("<b>").append(title).append("</b>"); if (!subtitle.equals("")) text.append("<br/>").append(subtitle); if (!author.equals("")) text.append("<br/>").append(author); if (!year.equals("")) text.append("<br/>").append(year); if (!series.equals("")) text.append("<br/>").append(series); result.setInnerhtml(text.toString()); // ID Matcher matcher = idPattern.matcher(element.html()); if (matcher.find()) { result.setId(matcher.group(2)); } else { matcher = weakIdPattern.matcher(element.html()); if (matcher.find()) { result.setId(matcher.group(2)); } } // Availability if (result.getId() != null) { String url = opac_url + "/DesktopModules/OCLC.OPEN.PL.DNN.SearchModule/SearchService" + ".asmx/GetAvailability"; String culture = element.select("input[name$=culture]").val(); JSONObject data = new JSONObject(); try { // Determine portalID value int portalId = 1; for (Element scripttag : doc.select("script")) { String scr = scripttag.html(); if (scr.contains("LoadSharedCatalogueViewAvailabilityAsync")) { Pattern portalIdPattern = Pattern.compile( ".*LoadSharedCatalogueViewAvailabilityAsync\\([^,]*,[^,]*," + "[^0-9,]*([0-9]+)[^0-9,]*,.*\\).*"); Matcher portalIdMatcher = portalIdPattern.matcher(scr); if (portalIdMatcher.find()) { portalId = Integer.parseInt(portalIdMatcher.group(1)); } } } data.put("portalId", portalId).put("mednr", result.getId()) .put("culture", culture).put("requestCopyData", false) .put("branchFilter", ""); StringEntity entity = new StringEntity(data.toString()); entity.setContentType(ContentType.APPLICATION_JSON.getMimeType()); String json = httpPost(url, entity, getDefaultEncoding()); JSONObject availabilityData = new JSONObject(json); String isAvail = availabilityData.getJSONObject("d").getString("IsAvail"); switch (isAvail) { case "true": result.setStatus(SearchResult.Status.GREEN); break; case "false": result.setStatus(SearchResult.Status.RED); break; case "digital": result.setStatus(SearchResult.Status.UNKNOWN); break; } } catch (JSONException | IOException e) { e.printStackTrace(); } } result.setNr(i); results.add(result); } return new SearchRequestResult(results, totalCount, page); } private String getCoverUrl(Element img) { String[] parts = img.attr("sources").split("\\|"); // Example: SetSimpleCover|a|https://vlb.de/GetBlob.aspx?strIsbn=9783868511291& // size=S|a|http://www.buchhandel.de/default.aspx?strframe=titelsuche& // caller=vlbPublic&func=DirectIsbnSearch&isbn=9783868511291& // nSiteId=11|c|SetNoCover|a|/DesktopModules/OCLC.OPEN.PL.DNN // .BaseLibrary/StyleSheets/Images/Fallbacks/emptyURL.gif?4.2.0.0|a| for (int i = 0; i + 2 < parts.length; i++) { if (parts[i].equals("SetSimpleCover")) { String url = parts[i + 2].replace("&", "&"); try { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("HEAD"); int code = conn.getResponseCode(); if (code == 200) { return url; } } catch (IOException e) { e.printStackTrace(); } } } if (img.hasAttr("devsources")) { img.attr("devsources").split("\\|"); for (int i = 0; i + 2 < parts.length; i++) { if (parts[i].equals("SetSimpleCover")) { String url = parts[i + 2].replace("&", "&"); try { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("HEAD"); int code = conn.getResponseCode(); if (code == 200) { return url; } } catch (IOException e) { e.printStackTrace(); } } } } return null; } private String numberToText(int number) { switch (number) { case 1: return "First"; case 2: return "Second"; case 3: return "Third"; default: return null; } } @Override public SearchRequestResult filterResults(Filter filter, Filter.Option option) throws IOException, OpacErrorException { return null; } @Override public SearchRequestResult searchGetPage(int page) throws IOException, OpacErrorException, JSONException { if (searchResultDoc == null) throw new NotReachableException(); Document doc = searchResultDoc; if (doc.select("span[id$=DataPager1]").size() == 0) { /* New style: Page buttons using normal links We can go directly to the correct page */ if (doc.select("a[id*=LinkButtonPageN]").size() > 0) { String href = doc.select("a[id*=LinkButtonPageN][href*=page]").first().attr("href"); String url = href.replaceFirst("page=\\d+", "page=" + page); Document doc2 = Jsoup.parse(httpGet(url, getDefaultEncoding())); return parse_search(doc2, page); } else { int totalCount; try { totalCount = Integer.parseInt( doc.select("span[id$=TotalItemsLabel]").first().text()); } catch (Exception e) { totalCount = 0; } // Next page does not exist return new SearchRequestResult( new ArrayList<SearchResult>(), 0, totalCount ); } } else { /* Old style: Page buttons using Javascript When there are many pages of results, there will only be links to the next 4 and previous 4 pages, so we will click links until it gets to the correct page. */ Elements pageLinks = doc.select("span[id$=DataPager1]").first() .select("a[id*=LinkButtonPageN], span[id*=LabelPageN]"); int from = Integer.valueOf(pageLinks.first().text()); int to = Integer.valueOf(pageLinks.last().text()); Element linkToClick; boolean willBeCorrectPage; if (page < from) { linkToClick = pageLinks.first(); willBeCorrectPage = false; } else if (page > to) { linkToClick = pageLinks.last(); willBeCorrectPage = false; } else { linkToClick = pageLinks.get(page - from); willBeCorrectPage = true; } if (linkToClick.tagName().equals("span")) { // we are trying to get the page we are already on return parse_search(searchResultDoc, page); } Pattern pattern = Pattern.compile("javascript:__doPostBack\\('([^,]*)','([^\\)]*)'\\)"); Matcher matcher = pattern.matcher(linkToClick.attr("href")); if (!matcher.find()) throw new OpacErrorException(StringProvider.INTERNAL_ERROR); FormElement form = (FormElement) doc.select("form").first(); HttpEntity data = formData(form, null).addTextBody("__EVENTTARGET", matcher.group(1)) .addTextBody("__EVENTARGUMENT", matcher.group(2)) .build(); String postUrl = form.attr("abs:action"); String html = httpPost(postUrl, data, "UTF-8"); if (willBeCorrectPage) { // We clicked on the correct link Document doc2 = Jsoup.parse(html); doc2.setBaseUri(postUrl); return parse_search(doc2, page); } else { // There was no correct link, so try to find one again searchResultDoc = Jsoup.parse(html); searchResultDoc.setBaseUri(postUrl); return searchGetPage(page); } } } @Override public DetailedItem getResultById(String id, String homebranch) throws IOException, OpacErrorException { try { String html = httpGet(opac_url + "/" + data.getJSONObject("urls").getString("simple_search") + NO_MOBILE + "&id=" + id, getDefaultEncoding()); return parse_result(Jsoup.parse(html)); } catch (JSONException e) { throw new IOException(e.getMessage()); } } protected DetailedItem parse_result(Document doc) { DetailedItem item = new DetailedItem(); // Title and Subtitle item.setTitle(doc.select("span[id$=LblShortDescriptionValue]").text()); String subtitle = doc.select("span[id$=LblSubTitleValue]").text(); if (subtitle.equals("") && doc.select("span[id$=LblShortDescriptionValue]").size() > 0) { // Subtitle detection for Bern Element next = doc.select("span[id$=LblShortDescriptionValue]").first().parent().nextElementSibling(); if (next.select("span").size() == 0) { subtitle = next.text().trim(); } } if (!subtitle.equals("")) { item.addDetail(new Detail(stringProvider.getString(StringProvider.SUBTITLE), subtitle)); } // Cover if (doc.select("input[id$=mediumImage]").size() > 0) { item.setCover(doc.select("input[id$=mediumImage]").attr("src")); } else if (doc.select("img[id$=CoverView_Image]").size() > 0) { item.setCover(getCoverUrl(doc.select("img[id$=CoverView_Image]").first())); } // ID item.setId(doc.select("input[id$=regionmednr]").val()); // Description if (doc.select("span[id$=ucCatalogueContent_LblAnnotation]").size() > 0) { String name = doc.select("span[id$=lblCatalogueContent]").text(); String value = doc.select("span[id$=ucCatalogueContent_LblAnnotation]").text(); item.addDetail(new Detail(name, value)); } // Details String DETAIL_SELECTOR = "div[id$=CatalogueDetailView] .spacingBottomSmall:has(span+span)," + "div[id$=CatalogueDetailView] .spacingBottomSmall:has(span+a)"; for (Element detail : doc.select(DETAIL_SELECTOR)) { String name = detail.select("span").get(0).text().replace(": ", ""); String value = ""; if (detail.select("a").size() > 1) { int i = 0; for (Element a : detail.select("a")) { if (i != 0) { value += ", "; } value += a.text().trim(); i++; } } else { value = detail.select("span, a").get(1).text(); } item.addDetail(new Detail(name, value)); } // Copies Element table = doc.select("table[id$=grdViewMediumCopies]").first(); if (table != null) { Elements trs = table.select("tr"); List<String> columnmap = new ArrayList<>(); for (Element th : trs.first().select("th")) { columnmap.add(getCopyColumnKey(th.text())); } DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); for (int i = 1; i < trs.size(); i++) { Elements tds = trs.get(i).select("td"); Copy copy = new Copy(); for (int j = 0; j < tds.size(); j++) { if (columnmap.get(j) == null) continue; String text = tds.get(j).text().replace("\u00a0", ""); if (text.equals("")) continue; copy.set(columnmap.get(j), text, fmt); } item.addCopy(copy); } } return item; } protected String getCopyColumnKey(String text) { switch (text) { case "Zweigstelle": case "Bibliothek": return "branch"; case "Standorte": case "Standort": return "location"; case "Status": return "status"; case "Vorbestellungen": return "reservations"; case "Frist": case "Rückgabedatum": return "returndate"; case "Signatur": return "signature"; default: return null; } } @Override public DetailedItem getResult(int position) throws IOException, OpacErrorException { return null; } @Override public ReservationResult reservation(DetailedItem item, Account account, int useraction, String selection) throws IOException { return null; } @Override public ProlongResult prolong(String media, Account account, int useraction, String selection) throws IOException { return null; } @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 { return null; } @Override public AccountData account(Account account) throws IOException, JSONException, OpacErrorException { return null; } @Override public void checkAccountData(Account account) throws IOException, JSONException, OpacErrorException { } @Override public List<SearchField> parseSearchFields() throws IOException, OpacErrorException, JSONException { String url = opac_url + "/" + data.getJSONObject("urls").getString("advanced_search") + NO_MOBILE; Document doc = Jsoup.parse(httpGet(url, getDefaultEncoding())); Element table = doc.select(".ModOPENExtendedSearchModuleC table").first(); List<SearchField> fields = new ArrayList<>(); JSONObject selectable = new JSONObject(); selectable.put("selectable", true); JSONObject notSelectable = new JSONObject(); notSelectable.put("selectable", false); // Selectable search criteria Elements options = table.select("select[id$=FirstSearchField] option"); for (Element option : options) { TextSearchField field = new TextSearchField(); field.setId(option.val()); field.setDisplayName(option.text()); field.setData(selectable); fields.add(field); } // More criteria Element moreHeader = table.select("span[id$=LblMoreCriterias]").parents().select("tr").first(); if (moreHeader != null) { Elements siblings = moreHeader.siblingElements(); int startIndex = moreHeader.elementSiblingIndex(); for (int i = startIndex; i < siblings.size(); i++) { Element tr = siblings.get(i); if (tr.select("input, select").size() == 0) continue; if (tr.select("input[type=text]").size() == 1) { Element input = tr.select("input[type=text]").first(); TextSearchField field = new TextSearchField(); field.setId(input.attr("name")); field.setDisplayName(tr.select("span[id*=Lbl]").first().text()); field.setData(notSelectable); if (tr.text().contains("nur Ziffern")) field.setNumber(true); fields.add(field); } else if (tr.select("input[type=text]").size() == 2) { Element input1 = tr.select("input[type=text]").get(0); Element input2 = tr.select("input[type=text]").get(1); TextSearchField field1 = new TextSearchField(); field1.setId(input1.attr("name")); field1.setDisplayName(tr.select("span[id*=Lbl]").first().text()); field1.setData(notSelectable); if (tr.text().contains("nur Ziffern")) field1.setNumber(true); fields.add(field1); TextSearchField field2 = new TextSearchField(); field2.setId(input2.attr("name")); field2.setDisplayName(tr.select("span[id*=Lbl]").first().text()); field2.setData(notSelectable); field2.setHalfWidth(true); if (tr.text().contains("nur Ziffern")) field2.setNumber(true); fields.add(field2); } else if (tr.select("select").size() == 1) { Element select = tr.select("select").first(); DropdownSearchField dropdown = new DropdownSearchField(); dropdown.setId(select.attr("name")); dropdown.setDisplayName(tr.select("span[id*=Lbl]").first().text()); List<DropdownSearchField.Option> values = new ArrayList<>(); for (Element option : select.select("option")) { DropdownSearchField.Option opt = new DropdownSearchField.Option(option.val(), option.text()); values.add(opt); } dropdown.setDropdownValues(values); fields.add(dropdown); } else if (tr.select("input[type=checkbox]").size() == 1) { Element checkbox = tr.select("input[type=checkbox]").first(); CheckboxSearchField field = new CheckboxSearchField(); field.setId(checkbox.attr("name")); field.setDisplayName(tr.select("span[id*=Lbl]").first().text()); fields.add(field); } } } return fields; } @Override public String getShareUrl(String id, String title) { return opac_url + "/Permalink.aspx" + "?id" + "=" + id; } @Override public int getSupportFlags() { return SUPPORT_FLAG_ENDLESS_SCROLLING; } @Override public Set<String> getSupportedLanguages() throws IOException { return null; } @Override public void setLanguage(String language) { } @Override protected String getDefaultEncoding() { try { if (data.has("charset")) { return data.getString("charset"); } } catch (JSONException e) { e.printStackTrace(); } return "UTF-8"; } /** * Better version of JSoup's implementation of this function ({@link * org.jsoup.nodes.FormElement#formData()}). * * @param form The form to submit * @param submitName The name attribute of the button which is clicked to submit the form, or * null * @return A MultipartEntityBuilder containing the data of the form */ protected MultipartEntityBuilder formData(FormElement form, String submitName) { MultipartEntityBuilder data = MultipartEntityBuilder.create(); data.setLaxMode(); // data.setCharset somehow breaks everything in Bern. // data.addTextBody breaks utf-8 characters in select boxes in Bern // .getBytes is an implicit, undeclared UTF-8 conversion, this seems to work -- at least in Bern // iterate the form control elements and accumulate their values for (Element el : form.elements()) { if (!el.tag().isFormSubmittable()) { continue; // contents are form listable, superset of submitable } String name = el.attr("name"); if (name.length() == 0) continue; String type = el.attr("type"); if ("select".equals(el.tagName())) { Elements options = el.select("option[selected]"); boolean set = false; for (Element option : options) { data.addBinaryBody(name, option.val().getBytes()); set = true; } if (!set) { Element option = el.select("option").first(); if (option != null) { data.addBinaryBody(name, option.val().getBytes()); } } } else if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) { // only add checkbox or radio if they have the checked attribute if (el.hasAttr("checked")) { data.addBinaryBody(name, el.val().length() > 0 ? el.val().getBytes() : "on".getBytes()); } } else if ("submit".equalsIgnoreCase(type) || "image".equalsIgnoreCase(type) || "button".equalsIgnoreCase(type)) { if (submitName != null && el.attr("name").contains(submitName)) { data.addBinaryBody(name, el.val().getBytes()); } } else { data.addBinaryBody(name, el.val().getBytes()); } } return data; } }