/** * Copyright (C) 2013 by Raphael Michel 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.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.utils.URLEncodedUtils; 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.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; import org.jsoup.select.Elements; import java.io.IOException; import java.io.UnsupportedEncodingException; 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.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.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.MediaType; 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 für Web-Opacs von Zones mit dem Hinweis "Zones.2.2.45.xx" oder "ZONES v1.8.1" im Footer. * * TODO: Kontofunktionen für Zones 1.8.1 */ public class Zones extends BaseApi { private static HashMap<String, MediaType> defaulttypes = new HashMap<>(); static { // Zones 2.2 defaulttypes.put("Buch", MediaType.BOOK); defaulttypes.put("Buch/Druckschrift", MediaType.BOOK); defaulttypes.put("Buch Erwachsene", MediaType.BOOK); defaulttypes.put("Buch Kinder/Jugendliche", MediaType.BOOK); defaulttypes.put("Buch Kinder/Jugend", MediaType.BOOK); defaulttypes.put("Kinder-Buch", MediaType.BOOK); defaulttypes.put("DVD", MediaType.DVD); defaulttypes.put("Kinder-DVD", MediaType.DVD); defaulttypes.put("Konsolenspiele", MediaType.GAME_CONSOLE); defaulttypes.put("Blu-ray Disc", MediaType.BLURAY); defaulttypes.put("Compact Disc", MediaType.CD); defaulttypes.put("CD-ROM", MediaType.CD_SOFTWARE); defaulttypes.put("Kinder-CD", MediaType.CD_SOFTWARE); defaulttypes.put("Noten", MediaType.SCORE_MUSIC); defaulttypes.put("Note", MediaType.SCORE_MUSIC); defaulttypes.put("Zeitschrift, Heft", MediaType.MAGAZINE); defaulttypes.put("Zeitschrift", MediaType.MAGAZINE); defaulttypes.put("E-Book", MediaType.EBOOK); defaulttypes.put("CDROM", MediaType.CD_SOFTWARE); defaulttypes.put("E-Audio", MediaType.MP3); defaulttypes.put("CD", MediaType.CD); defaulttypes.put("eBook", MediaType.EBOOK); defaulttypes.put("ePaper", MediaType.EBOOK); defaulttypes.put("Plan, Karte", MediaType.MAP); // Zones 1.8.1 (.gif file names) defaulttypes.put("book", MediaType.BOOK); defaulttypes.put("cd", MediaType.CD); defaulttypes.put("video", MediaType.MOVIE); defaulttypes.put("serial", MediaType.MAGAZINE); } // Indicates whether the OPAC uses ZONES 1.8.1 (instead of 2.2) private boolean version18; private String opac_url = ""; private JSONObject data; private int page; private String searchobj; private String accountobj; @Override public List<SearchField> parseSearchFields() throws IOException { if (!initialised) start(); List<SearchField> fields = new ArrayList<>(); String html = httpGet( opac_url + "/APS_ZONES?fn=AdvancedSearch&Style=Portal3&SubStyle=&Lang=GER" + "&ResponseEncoding=utf-8", getDefaultEncoding()); Document doc = Jsoup.parse(html); // find text fields Elements txt_opts = doc.select("#formSelectTerm_1 option"); for (Element opt : txt_opts) { TextSearchField field = new TextSearchField(); field.setId(opt.attr("value")); field.setHint(""); field.setDisplayName(opt.text()); fields.add(field); } // find filters String filtersQuery = version18 ? ".inSearchLimits .floatingBox" : ".TabRechAv .limitBlock"; Elements filters = doc.select(filtersQuery); int i = 0; for (Element filter : filters) { DropdownSearchField dropdown = new DropdownSearchField(); dropdown.addDropdownValue("", "Alle"); // All dropdowns use "q.limits.limit" as URL param, but they must not have the same ID dropdown.setId("dropdown_" + i); if (version18) { dropdown.setDisplayName(filter.select("tr").get(0).text().trim()); Elements opts = filter.select("tr").get(1).select("table td:has(input)"); for (Element opt : opts) { dropdown.addDropdownValue(opt.select("input").attr("value"), opt.text().trim()); } } else { dropdown.setDisplayName(filter.parent().previousElementSibling().text().trim()); Elements opts = filter.select(".limitChoice label"); for (Element opt : opts) { dropdown.addDropdownValue(opt.attr("for"), opt.text().trim()); } } fields.add(dropdown); i++; } return fields; } @Override public void start() throws IOException { String html = httpGet( opac_url + "/APS_ZONES?fn=AdvancedSearch&Style=Portal3&SubStyle=&Lang=GER" + "&ResponseEncoding=utf-8", getDefaultEncoding()); Document doc = Jsoup.parse(html); searchobj = doc.select("#ExpertSearch").attr("action"); version18 = doc.select(".poweredBy").text().contains("v1.8"); super.start(); } @Override public void init(Library lib, HttpClientFactory httpClientFactory) { super.init(lib, httpClientFactory); this.library = lib; this.data = lib.getData(); try { this.opac_url = data.getString("baseurl"); } catch (JSONException e) { throw new RuntimeException(e); } } private int addParameters(SearchQuery query, List<NameValuePair> params, int index) { if (query.getValue().equals("")) { return index; } if (query.getSearchField() instanceof TextSearchField) { if (index != 1) { params.add(new BasicNameValuePair(".form.t" + index + ".logic", "and")); } params.add(new BasicNameValuePair("q.form.t" + index + ".term", query.getKey())); params.add(new BasicNameValuePair("q.form.t" + index + ".expr", query.getValue())); return index + 1; } else if (query.getSearchField() instanceof DropdownSearchField) { params.add(new BasicNameValuePair("q.limits.limit", query.getValue())); } return index; } @Override public SearchRequestResult search(List<SearchQuery> queries) throws IOException, OpacErrorException { start(); List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("Style", version18 ? "Portal2" : "Portal3")); params.add(new BasicNameValuePair("SubStyle", "")); params.add(new BasicNameValuePair("Lang", "GER")); params.add(new BasicNameValuePair("ResponseEncoding", "utf-8")); params.add(new BasicNameValuePair("Method", "QueryWithLimits")); params.add(new BasicNameValuePair("SearchType", "AdvancedSearch")); params.add(new BasicNameValuePair("TargetSearchType", "AdvancedSearch")); params.add(new BasicNameValuePair("DB", "SearchServer")); params.add(new BasicNameValuePair("q.PageSize", "10")); int index = 1; for (SearchQuery query : queries) { index = addParameters(query, params, index); } if (index > 3) { throw new OpacErrorException(stringProvider.getQuantityString( StringProvider.LIMITED_NUM_OF_CRITERIA, 3, 3)); } else if (index == 1) { throw new OpacErrorException( stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)); } String html = httpGet(opac_url + "/" + searchobj + "?" + URLEncodedUtils.format(params, "UTF-8"), getDefaultEncoding()); page = 1; return parse_search(html, page); } @Override public SearchRequestResult searchGetPage(int page) throws IOException, OpacErrorException { List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("Style", version18 ? "Portal2" : "Portal3")); params.add(new BasicNameValuePair("SubStyle", "")); params.add(new BasicNameValuePair("Lang", "GER")); params.add(new BasicNameValuePair("ResponseEncoding", "utf-8")); if (page > this.page) { params.add(new BasicNameValuePair("Method", version18 ? "FetchIncrementalBrowseDown" : "PageDown")); } else { params.add(new BasicNameValuePair("Method", version18 ? "FetchIncrementalBrowseUp" : "PageUp")); } params.add(new BasicNameValuePair("PageSize", "10")); String html = httpGet(opac_url + "/" + searchobj + "?" + URLEncodedUtils.format(params, "UTF-8"), getDefaultEncoding()); this.page = page; return parse_search(html, page); } private SearchRequestResult parse_search(String html, int page) throws OpacErrorException { Document doc = Jsoup.parse(html); doc.setBaseUri(opac_url + "/APS_PRESENT_BIB"); if (doc.select("#ErrorAdviceRow").size() > 0) { throw new OpacErrorException(doc.select("#ErrorAdviceRow").text() .trim()); } int results_total = -1; String searchHitsQuery = version18 ? "td:containsOwn(Total)" : ".searchHits"; if (doc.select(searchHitsQuery).size() > 0) { results_total = Integer.parseInt(doc.select(searchHitsQuery).first().text().trim() .replaceAll(".*\\(([0-9]+)\\).*", "$1")); } else if (doc.select("span:matches(\\[\\d+/\\d+\\])").size() > 0) { // Zones 1.8 - searchGetPage String text = doc.select("span:matches(\\[\\d+/\\d+\\])").text(); Pattern pattern = Pattern.compile("\\[\\d+/(\\d+)\\]"); Matcher matcher = pattern.matcher(text); if (matcher.find()) { results_total = Integer.parseInt(matcher.group(1)); } } if (doc.select(".pageNavLink").size() > 0) { // Zones 2.2 searchobj = doc.select(".pageNavLink").first().attr("href").split("\\?")[0]; } else if (doc.select("div[targetObject]").size() > 0) { // Zones 1.8 - search searchobj = doc.select("div[targetObject]").attr("targetObject").split("\\?")[0]; } else { // Zones 1.8 - searchGetPage // The page contains a data structure that at first glance seems to be JSON, but uses // "=" instead of ":". So we parse it using regex... Pattern pattern = Pattern.compile("targetObject = \"([^\\?]+)[^\"]+\""); Matcher matcher = pattern.matcher(doc.html()); if (matcher.find()) { searchobj = matcher.group(1); } } Elements table = doc.select( "#BrowseList > tbody > tr," // Zones 2.2 + " .inRoundBox1" // Zones 1.8 ); List<SearchResult> results = new ArrayList<>(); for (int i = 0; i < table.size(); i++) { Element tr = table.get(i); SearchResult sr = new SearchResult(); String typetext; if (version18) { String[] parts = tr.select("img[src^=IMG/MAT]").attr("src").split("/"); typetext = parts[parts.length - 1].replace(".gif", ""); } else { typetext = tr.select(".SummaryMaterialTypeField").text().replace("\n", " ").trim(); } if (data.has("mediatypes")) { try { sr.setType(MediaType.valueOf(data.getJSONObject( "mediatypes").getString(typetext))); } catch (JSONException | IllegalArgumentException e) { sr.setType(defaulttypes.get(typetext)); } } else { sr.setType(defaulttypes.get(typetext)); } String imgUrl = null; if (version18) { if (tr.select("a[title=Titelbild]").size() > 0) { imgUrl = tr.select("a[title=Titelbild]").attr("href"); } else if (tr.select("img[width=50]").size() > 0) { // TODO: better way to select these cover images? (found in Hannover) imgUrl = tr.select("img[width=50]").attr("src"); } } else { if (tr.select(".SummaryImageCell img[id^=Bookcover]").size() > 0) { imgUrl = tr.select(".SummaryImageCell img[id^=Bookcover]").first().attr("src"); } } sr.setCover(imgUrl); if (version18) { if (tr.select("img[src$=oci_1.gif]").size() > 0) { // probably can only appear when searching the catalog on a terminal in // the library. sr.setStatus(SearchResult.Status.GREEN); } else if (tr.select("img[src$=blob_amber.gif]").size() > 0) { sr.setStatus(SearchResult.Status.YELLOW); } } String desc = ""; String childrenQuery = version18 ? "table[cellpadding=1] tr" : ".SummaryDataCell tr, .SummaryDataCellStripe tr"; Elements children = tr.select(childrenQuery); int childrennum = children.size(); boolean haslink = false; for (int ch = 0; ch < childrennum; ch++) { Element node = children.get(ch); if (getName(node).equals("Titel")) { desc += "<b>" + getValue(node).trim() + "</b><br />"; } else if (getName(node).equals("Verfasser") || getName(node).equals("Jahr")) { desc += getValue(node).trim() + "<br />"; } String linkSelector = version18 ? "a[href*=ShowStock], a[href*=APS_CAT_IDENTIFY]" : ".SummaryFieldData a.SummaryFieldLink"; if (node.select(linkSelector).size() > 0 && !haslink) { String href = node.select(linkSelector).attr("abs:href"); Map<String, String> hrefq = getQueryParamsFirst(href); if (hrefq.containsKey("no")) { sr.setId(hrefq.get("no")); } else if (hrefq.containsKey("Key")) { sr.setId(hrefq.get("Key")); } haslink = true; } } if (desc.endsWith("<br />")) { desc = desc.substring(0, desc.length() - 6); } sr.setInnerhtml(desc); sr.setNr(i); results.add(sr); } return new SearchRequestResult(results, results_total, page); } private String getValue(Element node) { if (version18) { return node.child(4).text().trim(); } else { return node.select(".SummaryFieldData").text(); } } private String getName(Element node) { if (version18) { return node.child(0).text().trim(); } else { return node.select(".SummaryFieldLegend").text(); } } @Override public DetailedItem getResultById(String id, String homebranch) throws IOException { List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("Style", version18 ? "Portal2" : "Portal3")); params.add(new BasicNameValuePair("SubStyle", "")); params.add(new BasicNameValuePair("Lang", "GER")); params.add(new BasicNameValuePair("ResponseEncoding", "utf-8")); params.add(new BasicNameValuePair("no", id)); String html = httpGet( opac_url + "/APS_PRESENT_BIB?" + URLEncodedUtils.format(params, "UTF-8"), getDefaultEncoding()); return parse_result(id, html); } @Override public DetailedItem getResult(int nr) throws IOException { return null; } private DetailedItem parse_result(String id, String html) { Document doc = Jsoup.parse(html); DetailedItem result = new DetailedItem(); result.setTitle(""); boolean title_is_set = false; result.setId(id); String detailTrsQuery = version18 ? ".inRoundBox1 table table tr" : ".DetailDataCell table table:not(.inRecordHeader) tr"; Elements detailtrs1 = doc.select(detailTrsQuery); for (int i = 0; i < detailtrs1.size(); i++) { Element tr = detailtrs1.get(i); int s = tr.children().size(); if (tr.child(0).text().trim().equals("Titel") && !title_is_set) { result.setTitle(tr.child(s - 1).text().trim()); title_is_set = true; } else if (s > 1) { Element valchild = tr.child(s - 1); if (valchild.select("table").isEmpty()) { String val = valchild.text().trim(); if (val.length() > 0) { result.addDetail(new Detail(tr.child(0).text().trim(), val)); } } } } for (Element a : doc.select("a.SummaryActionLink")) { if (a.text().contains("Vormerken")) { result.setReservable(true); result.setReservation_info(a.attr("href")); } } Elements detaildiv = doc.select("div.record-item-new"); if (!detaildiv.isEmpty()) { for (int i = 0; i < detaildiv.size(); i++) { Element dd = detaildiv.get(i); String text = ""; for (Node node : dd.childNodes()) { if (node instanceof TextNode) { String snip = ((TextNode) node).text(); if (snip.length() > 0) { text += snip; } } else if (node instanceof Element) { if (((Element) node).tagName().equals("br")) { text += "\n"; } else { String snip = ((Element) node).text().trim(); if (snip.length() > 0) { text += snip; } } } } result.addDetail(new Detail("", text)); } } if (doc.select("span.z3988").size() > 0) { // Sometimes there is a <span class="Z3988"> item which provides // data in a standardized format. String z3988data = doc.select("span.z3988").first().attr("title") .trim(); for (String pair : z3988data.split("&")) { String[] nv = pair.split("=", 2); if (nv.length == 2) { if (!nv[1].trim().equals("")) { if (nv[0].equals("rft.btitle") && result.getTitle().length() == 0) { result.setTitle(nv[1]); } else if (nv[0].equals("rft.atitle") && result.getTitle().length() == 0) { result.setTitle(nv[1]); } else if (nv[0].equals("rft.au")) { result.addDetail(new Detail("Author", nv[1])); } } } } } // Cover if (doc.select(".BookCover, .LargeBookCover").size() > 0) { result.setCover(doc.select(".BookCover, .LargeBookCover").first().attr("src")); } Elements copydivs = doc.select("div[id^=stock_]"); String pop = ""; for (int i = 0; i < copydivs.size(); i++) { Element div = copydivs.get(i); if (div.attr("id").startsWith("stock_head")) { pop = div.text().trim(); continue; } Copy copy = new Copy(); DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); // This is getting very ugly - check if it is valid for libraries which are not Hamburg. // Seems to also work in Kiel (Zones 1.8, checked 10.10.2015) int j = 0; for (Node node : div.childNodes()) { try { if (node instanceof Element) { if (((Element) node).tag().getName().equals("br")) { copy.setBranch(pop); result.addCopy(copy); j = -1; } else if (((Element) node).tag().getName().equals("b") && j == 1) { copy.setLocation(((Element) node).text()); } else if (((Element) node).tag().getName().equals("b") && j > 1) { copy.setStatus(((Element) node).text()); } j++; } else if (node instanceof TextNode) { if (j == 0) { copy.setDepartment(((TextNode) node).text()); } if (j == 2) { copy.setBarcode(((TextNode) node).getWholeText().trim() .split("\n")[0].trim()); } if (j == 6) { String text = ((TextNode) node).text().trim(); String date = text.substring(text.length() - 10); try { copy.setReturnDate(fmt.parseLocalDate(date)); } catch (IllegalArgumentException e) { e.printStackTrace(); } } j++; } } catch (Exception e) { e.printStackTrace(); } } } return result; } @Override public ReservationResult reservation(DetailedItem item, Account acc, int useraction, String selection) throws IOException { String reservation_info = item.getReservation_info(); String html = httpGet(opac_url + "/" + reservation_info, getDefaultEncoding()); Document doc = Jsoup.parse(html); if (html.contains("Geheimnummer")) { List<NameValuePair> params = new ArrayList<>(); for (Element input : doc.select("#MainForm input")) { if (!input.attr("name").equals("BRWR") && !input.attr("name").equals("PIN")) { params.add(new BasicNameValuePair(input.attr("name"), input .attr("value"))); } } params.add(new BasicNameValuePair("BRWR", acc.getName())); params.add(new BasicNameValuePair("PIN", acc.getPassword())); html = httpGet( opac_url + "/" + doc.select("#MainForm").attr("action") + "?" + URLEncodedUtils.format(params, getDefaultEncoding()), getDefaultEncoding()); doc = Jsoup.parse(html); } if (useraction == ReservationResult.ACTION_BRANCH) { List<NameValuePair> params = new ArrayList<>(); for (Element input : doc.select("#MainForm input")) { if (!input.attr("name").equals("Confirm")) { params.add(new BasicNameValuePair(input.attr("name"), input .attr("value"))); } } params.add(new BasicNameValuePair( "MakeResTypeDef.Reservation.RecipientLocn", selection)); params.add(new BasicNameValuePair("Confirm", "1")); httpGet( opac_url + "/" + doc.select("#MainForm").attr("action") + "?" + URLEncodedUtils.format(params, getDefaultEncoding()), getDefaultEncoding()); return new ReservationResult(MultiStepResult.Status.OK); } if (useraction == 0) { ReservationResult res = null; for (Node n : doc.select("#MainForm").first().childNodes()) { if (n instanceof TextNode) { if (((TextNode) n).text().contains("Entgelt")) { res = new ReservationResult( ReservationResult.Status.CONFIRMATION_NEEDED); List<String[]> details = new ArrayList<>(); details.add(new String[]{((TextNode) n).text().trim()}); res.setDetails(details); res.setMessage(((TextNode) n).text().trim()); res.setActionIdentifier(MultiStepResult.ACTION_CONFIRMATION); } } } if (res != null) { return res; } } if (doc.select("#MainForm select").size() > 0) { ReservationResult res = new ReservationResult( ReservationResult.Status.SELECTION_NEEDED); List<Map<String, String>> sel = new ArrayList<>(); for (Element opt : doc.select("#MainForm select option")) { Map<String, String> selopt = new HashMap<>(); selopt.put("key", opt.attr("value")); selopt.put("value", opt.text().trim()); sel.add(selopt); } res.setSelection(sel); res.setMessage("Bitte Zweigstelle auswählen"); res.setActionIdentifier(ReservationResult.ACTION_BRANCH); return res; } return new ReservationResult(ReservationResult.Status.ERROR); } @Override public ProlongResult prolong(String media, Account account, int useraction, String Selection) throws IOException { if (media.startsWith("cannotrenew|")) { String message = media.split("\\|")[1]; return new ProlongResult(MultiStepResult.Status.ERROR, message); } if (accountobj == null) { try { login(account); } catch (OpacErrorException e) { return new ProlongResult(MultiStepResult.Status.ERROR, e.getMessage()); } } String html = httpGet(opac_url + "/" + media, getDefaultEncoding()); Document doc = Jsoup.parse(html); if ((html.contains("document.location.replace") || html .contains("Schnellsuche")) && useraction == 0) { try { login(account); } catch (OpacErrorException e) { return new ProlongResult(MultiStepResult.Status.ERROR, e.getMessage()); } prolong(media, account, 1, null); } String dialog = doc.select("#SSRenewDlgContent").text(); if (dialog.contains("erfolgreich")) { return new ProlongResult(MultiStepResult.Status.OK, dialog); } else { return new ProlongResult(MultiStepResult.Status.ERROR, dialog); } } @Override public CancelResult cancel(String media, Account account, int useraction, String selection) throws IOException, OpacErrorException { throw new UnsupportedOperationException(); } private Document login(Account acc) throws IOException, OpacErrorException { String html = httpGet( opac_url + "/APS_ZONES?fn=MyZone&Style=Portal3&SubStyle=&Lang=GER&ResponseEncoding" + "=utf-8", getDefaultEncoding()); Document doc = Jsoup.parse(html); doc.setBaseUri(opac_url + "/APS_ZONES"); if (doc.select(".AccountSummaryCounterLink").size() > 0) { return doc; } if (doc.select("#LoginForm").size() == 0) { throw new NotReachableException("Login form not found"); } List<NameValuePair> params = new ArrayList<>(); for (Element input : doc.select("#LoginForm input")) { if (!input.attr("name").equals("BRWR") && !input.attr("name").equals("PIN")) { params.add(new BasicNameValuePair(input.attr("name"), input .attr("value"))); } } params.add(new BasicNameValuePair("BRWR", acc.getName())); params.add(new BasicNameValuePair("PIN", acc.getPassword())); String loginHtml; try { loginHtml = httpPost( doc.select("#LoginForm").get(0).absUrl("action"), new UrlEncodedFormEntity(params), getDefaultEncoding()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } catch (IOException e) { e.printStackTrace(); return null; } if (!loginHtml.contains("Kontostand")) { throw new OpacErrorException(stringProvider.getString( StringProvider.LOGIN_FAILED)); } Document doc2 = Jsoup.parse(loginHtml); Pattern objid_pat = Pattern.compile("Obj_([0-9]+)\\?.*"); for (Element a : doc2.select("a")) { Matcher objid_matcher = objid_pat.matcher(a.attr("href")); if (objid_matcher.matches()) { accountobj = objid_matcher.group(1); } } return doc2; } @Override public AccountData account(Account acc) throws IOException, JSONException, OpacErrorException { Document login = login(acc); if (login == null) { return null; } AccountData res = new AccountData(acc.getId()); AccountLinks accountLinks = new AccountLinks(login, res); String lentLink = accountLinks.getLentLink(); String resLink = accountLinks.getResLink(); if (lentLink == null) { return null; } List<LentItem> lentItems = new ArrayList<>(); String lentUrl = opac_url + "/" + lentLink.replace("utf-8?Method", "utf-8&Method"); String lentHtml = httpGet(lentUrl, getDefaultEncoding()); Document lentDoc = Jsoup.parse(lentHtml); lentDoc.setBaseUri(lentUrl); loadMediaList(lentDoc, lentItems); res.setLent(lentItems); // In Koeln, the reservations link only doesn't show on the overview page if (resLink == null) { for (Element a : lentDoc.select("a.AccountMenuLink")) { if (a.text().contains("Vormerkungen")) { resLink = a.attr("href"); } } } List<ReservedItem> reservedItems = new ArrayList<>(); String resUrl = opac_url + "/" + resLink; String resHtml = httpGet(resUrl, getDefaultEncoding()); Document resDoc = Jsoup.parse(resHtml); resDoc.setBaseUri(resUrl); loadResList(resDoc, reservedItems); res.setReservations(reservedItems); return res; } private void loadMediaList(Document lentDoc, List<LentItem> items) throws IOException { items.addAll(parseMediaList(lentDoc)); String nextPageUrl = findNextPageUrl(lentDoc); if (nextPageUrl != null) { Document doc = Jsoup.parse(httpGet(nextPageUrl, getDefaultEncoding())); doc.setBaseUri(lentDoc.baseUri()); loadMediaList(doc, items); } } private void loadResList(Document resDoc, List<ReservedItem> items) throws IOException { items.addAll(parseResList(resDoc)); String nextPageUrl = findNextPageUrl(resDoc); if (nextPageUrl != null) { Document doc = Jsoup.parse(httpGet(nextPageUrl, getDefaultEncoding())); doc.setBaseUri(resDoc.baseUri()); loadResList(doc, items); } } static String findNextPageUrl(Document doc) { if (doc.select(".pageNavLink[title*=nächsten]").size() > 0) { Element link = doc.select(".pageNavLink[title*=nächsten]").first(); return link.absUrl("href"); } else { return null; } } static List<ReservedItem> parseResList(Document doc) { List<ReservedItem> reservations = new ArrayList<>(); for (Element table : doc .select(".MessageBrowseItemDetailsCell table, " + ".MessageBrowseItemDetailsCellStripe" + " table")) { ReservedItem item = new ReservedItem(); for (Element tr : table.select("tr")) { String desc = tr.select(".MessageBrowseFieldNameCell").text() .trim(); String value = tr.select(".MessageBrowseFieldDataCell").text() .trim(); if (desc.equals("Titel")) item.setTitle(value); if (desc.equals("Publikationsform")) item.setFormat(value); if (desc.equals("Liefern an")) item.setBranch(value); if (desc.equals("Status")) item.setStatus(value); } if ("Gelöscht".equals(item.getStatus())) continue; reservations.add(item); } return reservations; } static List<LentItem> parseMediaList(Document doc) { List<LentItem> lent = new ArrayList<>(); DateTimeFormatter fmt = DateTimeFormat.forPattern("dd/MM/yyyy").withLocale(Locale.GERMAN); Pattern id_pat = Pattern.compile("javascript:renewItem\\('[0-9]+','(.*)'\\)"); Pattern cannotrenew_pat = Pattern.compile("javascript:CannotRenewLoan\\('[0-9]+','(.*)','[0-9]+'\\)"); for (Element table : doc .select(".LoansBrowseItemDetailsCellStripe table, " + ".LoansBrowseItemDetailsCell " + "table")) { LentItem item = new LentItem(); for (Element tr : table.select("tr")) { String desc = tr.select(".LoanBrowseFieldNameCell").text() .trim(); String value = tr.select(".LoanBrowseFieldDataCell").text() .trim(); if (desc.equals("Titel")) { item.setTitle(value); if (tr.select(".LoanBrowseFieldDataCell a[href]").size() > 0) { String href = tr.select(".LoanBrowseFieldDataCell a[href]").attr("href"); Map<String, String> params = getQueryParamsFirst(href); if (params.containsKey("BACNO")) { item.setId(params.get("BACNO")); } } } if (desc.equals("Verfasser")) item.setAuthor(value); if (desc.equals("Mediennummer")) item.setBarcode(value); if (desc.equals("ausgeliehen in")) item.setHomeBranch(value); if (desc.matches("F.+lligkeits.*datum")) { value = value.split(" ")[0]; try { item.setDeadline(fmt.parseLocalDate(value)); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } if (table.select(".button[Title~=Zum]").size() == 1) { Matcher matcher1 = id_pat.matcher(table.select(".button[Title~=Zum]").attr("href")); if (matcher1.matches()) item.setProlongData(matcher1.group(1)); } else if (table.select(".CannotRenewLink").size() == 1) { Matcher matcher = cannotrenew_pat .matcher(table.select(".CannotRenewLink").attr("href").trim()); if (matcher.matches()) { item.setProlongData("cannotrenew|" + matcher.group(1)); } item.setRenewable(false); } lent.add(item); } return lent; } @Override public String getShareUrl(String id, String title) { List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("Style", "Portal3")); params.add(new BasicNameValuePair("SubStyle", "")); params.add(new BasicNameValuePair("Lang", "GER")); params.add(new BasicNameValuePair("ResponseEncoding", "utf-8")); params.add(new BasicNameValuePair("no", id)); return opac_url + "/APS_PRESENT_BIB?" + URLEncodedUtils.format(params, "UTF-8"); } @Override public int getSupportFlags() { return SUPPORT_FLAG_ENDLESS_SCROLLING | SUPPORT_FLAG_CHANGE_ACCOUNT; } @Override public ProlongAllResult prolongAll(Account account, int useraction, String selection) throws IOException { return null; } @Override public SearchRequestResult filterResults(Filter filter, Option option) { // TODO Auto-generated method stub return null; } @Override protected String getDefaultEncoding() { return "UTF-8"; } @Override public void checkAccountData(Account account) throws IOException, JSONException, OpacErrorException { Document login = login(account); if (login == null) { throw new NotReachableException("Login unsuccessful"); } } @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; } protected static class AccountLinks { private String lentLink; private String resLink; public AccountLinks(Document doc, AccountData res) { lentLink = null; resLink = null; for (Element td : doc .select(".AccountSummaryCounterNameCell, " + ".AccountSummaryCounterNameCellStripe, " + ".CAccountDetailFieldNameCellStripe, .CAccountDetailFieldNameCell")) { String section = td.text().trim(); if (section.contains("Entliehene Medien")) { lentLink = td.select("a").attr("href"); } else if (section.contains("Vormerkungen")) { resLink = td.select("a").attr("href"); } else if (section.contains("Kontostand")) { res.setPendingFees(td.nextElementSibling().text().trim()); } else if (section.matches("Ausweis g.ltig bis") || section.matches("Ausweis\u00a0g.ltig\u00a0bis")) { res.setValidUntil(td.nextElementSibling().text().trim()); } } for (Element a : doc.select("a.AccountMenuLink")) { if (a.text().contains("Ausleihen")) { lentLink = a.attr("href"); } else if (a.text().contains("Vormerkungen")) { resLink = a.attr("href"); } } } public String getLentLink() { return lentLink; } public String getResLink() { return resLink; } } }