/** * Copyright (C) 2014 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.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.parser.Parser; import org.jsoup.select.Elements; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; 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.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; /** * OpacApi implementation for Web Opacs of the TouchPoint product, developed by OCLC. */ public class TouchPoint extends BaseApi implements OpacApi { protected static HashMap<String, MediaType> defaulttypes = new HashMap<>(); static { defaulttypes.put("g", MediaType.EBOOK); defaulttypes.put("d", MediaType.CD); defaulttypes.put("buch", MediaType.BOOK); defaulttypes.put("bücher", MediaType.BOOK); defaulttypes.put("printmedien", MediaType.BOOK); defaulttypes.put("zeitschrift", MediaType.MAGAZINE); defaulttypes.put("zeitschriften", MediaType.MAGAZINE); defaulttypes.put("zeitung", MediaType.NEWSPAPER); defaulttypes.put( "Einzelband einer Serie, siehe auch übergeordnete Titel", MediaType.BOOK); defaulttypes.put("0", MediaType.BOOK); defaulttypes.put("1", MediaType.BOOK); defaulttypes.put("2", MediaType.BOOK); defaulttypes.put("3", MediaType.BOOK); defaulttypes.put("4", MediaType.BOOK); defaulttypes.put("5", MediaType.BOOK); defaulttypes.put("6", MediaType.SCORE_MUSIC); defaulttypes.put("7", MediaType.CD_MUSIC); defaulttypes.put("8", MediaType.CD_MUSIC); defaulttypes.put("tonträger", MediaType.CD_MUSIC); defaulttypes.put("12", MediaType.CD); defaulttypes.put("13", MediaType.CD); defaulttypes.put("cd", MediaType.CD); defaulttypes.put("dvd", MediaType.DVD); defaulttypes.put("14", MediaType.CD); defaulttypes.put("15", MediaType.DVD); defaulttypes.put("16", MediaType.CD); defaulttypes.put("audiocd", MediaType.CD); defaulttypes.put("film", MediaType.MOVIE); defaulttypes.put("filme", MediaType.MOVIE); defaulttypes.put("17", MediaType.MOVIE); defaulttypes.put("18", MediaType.MOVIE); defaulttypes.put("19", MediaType.MOVIE); defaulttypes.put("20", MediaType.DVD); defaulttypes.put("dvd", MediaType.DVD); defaulttypes.put("21", MediaType.SCORE_MUSIC); defaulttypes.put("noten", MediaType.SCORE_MUSIC); defaulttypes.put("22", MediaType.BLURAY); defaulttypes.put("23", MediaType.GAME_CONSOLE_PLAYSTATION); defaulttypes.put("26", MediaType.CD); defaulttypes.put("27", MediaType.CD); defaulttypes.put("28", MediaType.EBOOK); defaulttypes.put("31", MediaType.BOARDGAME); defaulttypes.put("35", MediaType.MOVIE); defaulttypes.put("36", MediaType.DVD); defaulttypes.put("37", MediaType.CD); defaulttypes.put("29", MediaType.AUDIOBOOK); defaulttypes.put("41", MediaType.GAME_CONSOLE); defaulttypes.put("42", MediaType.GAME_CONSOLE); defaulttypes.put("46", MediaType.GAME_CONSOLE_NINTENDO); defaulttypes.put("52", MediaType.EBOOK); defaulttypes.put("56", MediaType.EBOOK); defaulttypes.put("91", MediaType.EBOOK); defaulttypes.put("96", MediaType.EBOOK); defaulttypes.put("97", MediaType.EBOOK); defaulttypes.put("99", MediaType.EBOOK); defaulttypes.put("eb", MediaType.EBOOK); defaulttypes.put("ebook", MediaType.EBOOK); defaulttypes.put("buch01", MediaType.BOOK); defaulttypes.put("buch02", MediaType.PACKAGE_BOOKS); defaulttypes.put("medienpaket", MediaType.PACKAGE); defaulttypes.put("datenbank", MediaType.PACKAGE); defaulttypes .put("medienpaket, lernkiste, lesekiste", MediaType.PACKAGE); defaulttypes.put("buch03", MediaType.BOOK); defaulttypes.put("buch04", MediaType.PACKAGE_BOOKS); defaulttypes.put("buch05", MediaType.PACKAGE_BOOKS); defaulttypes.put("web-link", MediaType.URL); defaulttypes.put("ejournal", MediaType.EDOC); defaulttypes.put("karte", MediaType.MAP); } protected final long SESSION_LIFETIME = 1000 * 60 * 3; protected String opac_url = ""; protected JSONObject data; protected String CSId; protected String identifier; protected String reusehtml_reservation; protected int resultcount = 10; protected long logged_in; protected Account logged_in_as; protected String ENCODING = "UTF-8"; public List<SearchField> parseSearchFields() throws IOException, JSONException { if (!initialised) { start(); } String html = httpGet(opac_url + "/search.do?methodToCall=switchSearchPage&SearchType=2", ENCODING); Document doc = Jsoup.parse(html); List<SearchField> fields = new ArrayList<>(); Elements options = doc .select("select[name=searchCategories[0]] option"); for (Element option : options) { TextSearchField field = new TextSearchField(); field.setDisplayName(option.text()); field.setId(option.attr("value")); field.setHint(""); fields.add(field); } for (Element dropdown : doc.select(".accordion-body select")) { parseDropdown(dropdown, fields); } if (doc.select(".selectDatabase").size() > 0) { DropdownSearchField dropdown = new DropdownSearchField(); dropdown.setId("_database"); for (Element option : doc.select(".selectDatabase")) { String label = option.parent().ownText().trim(); if (label.equals("")) { for (Element a : option.siblingElements()) { label += a.ownText().trim(); } } dropdown.addDropdownValue(option.attr("name") + "=" + option.attr("value"), label.trim()); } dropdown.setDisplayName(doc.select(".dbselection h3").first().text().trim()); fields.add(dropdown); } return fields; } private void parseDropdown(Element dropdownElement, List<SearchField> fields) { Elements options = dropdownElement.select("option"); DropdownSearchField dropdown = new DropdownSearchField(); dropdown.setId(dropdownElement.attr("name")); // Some fields make no sense or are not supported in the app if (dropdown.getId().equals("numberOfHits") || dropdown.getId().equals("timeOut") || dropdown.getId().equals("rememberList")) { return; } for (Element option : options) { dropdown.addDropdownValue(option.attr("value"), option.text()); } dropdown.setDisplayName(dropdownElement.parent().select("label").text()); fields.add(dropdown); } @Override public void start() throws IOException { // Some libraries require start parameters for start.do, like Login=foo String startparams = ""; if (data.has("startparams")) { try { startparams = "?" + data.getString("startparams"); } catch (JSONException e) { e.printStackTrace(); } } String html = httpGet(opac_url + "/start.do" + startparams, ENCODING); initialised = true; Document doc = Jsoup.parse(html); CSId = doc.select("input[name=CSId]").val(); super.start(); } @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> query) throws IOException, OpacErrorException, JSONException { List<NameValuePair> params = new ArrayList<>(); boolean selectDatabase = false; int index = 0; start(); params.add(new BasicNameValuePair("methodToCall", "submitButtonCall")); params.add(new BasicNameValuePair("CSId", CSId)); params.add(new BasicNameValuePair("refine", "false")); params.add(new BasicNameValuePair("numberOfHits", "10")); for (SearchQuery entry : query) { if (entry.getValue().equals("")) { continue; } if (entry.getSearchField() instanceof DropdownSearchField) { if (entry.getKey().equals("_database")) { String[] parts = entry.getValue().split("=", 2); params.add(new BasicNameValuePair(parts[0], parts[1])); selectDatabase = true; } else { params.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } } else { if (index != 0) { params.add(new BasicNameValuePair("combinationOperator[" + index + "]", "AND")); } params.add(new BasicNameValuePair("searchCategories[" + index + "]", entry.getKey())); params.add(new BasicNameValuePair( "searchString[" + index + "]", entry.getValue())); index++; } } if (index == 0) { throw new OpacErrorException( stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)); } if (index > 4) { throw new OpacErrorException(stringProvider.getQuantityString( StringProvider.LIMITED_NUM_OF_CRITERIA, 4, 4)); } if (selectDatabase) { List<NameValuePair> selectParams = new ArrayList<>(); selectParams.addAll(params); selectParams.add(new BasicNameValuePair("methodToCallParameter", "selectDatabase")); httpGet(opac_url + "/search.do?" + URLEncodedUtils.format(selectParams, "UTF-8"), ENCODING); } params.add(new BasicNameValuePair("submitButtonCall_submitSearch", "Suchen")); params.add(new BasicNameValuePair("methodToCallParameter", "submitSearch")); String html = httpGet( opac_url + "/search.do?" + URLEncodedUtils.format(params, "UTF-8"), ENCODING); return parse_search_wrapped(html, 1); } public SearchRequestResult volumeSearch(Map<String, String> query) throws IOException, OpacErrorException { List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("methodToCall", "volumeSearch")); params.add(new BasicNameValuePair("dbIdentifier", query .get("dbIdentifier"))); params.add(new BasicNameValuePair("catKey", query.get("catKey"))); params.add(new BasicNameValuePair("periodical", "N")); String html = httpGet( opac_url + "/search.do?" + URLEncodedUtils.format(params, "UTF-8"), ENCODING); return parse_search_wrapped(html, 1); } @Override public SearchRequestResult searchGetPage(int page) throws IOException, OpacErrorException { if (!initialised) { start(); } String html = httpGet(opac_url + "/hitList.do?methodToCall=pos&identifier=" + identifier + "&curPos=" + (((page - 1) * resultcount) + 1), ENCODING); return parse_search_wrapped(html, page); } public class SingleResultFound extends Exception { } protected SearchRequestResult parse_search_wrapped(String html, int page) throws IOException, OpacErrorException { try { return parse_search(html, page); } catch (SingleResultFound e) { html = httpGet(opac_url + "/hitList.do?methodToCall=backToCompleteList&identifier=" + identifier, ENCODING); try { return parse_search(html, page); } catch (SingleResultFound e1) { throw new NotReachableException(); } } } protected SearchRequestResult parse_search(String html, int page) throws OpacErrorException, IOException, IOException, SingleResultFound { Document doc = Jsoup.parse(html); if (doc.select("#RefineHitListForm").size() > 0) { // the results are located on a different page loaded via AJAX html = httpGet( opac_url + "/speedHitList.do?_=" + String.valueOf(System.currentTimeMillis() / 1000) + "&hitlistindex=0&exclusionList=", ENCODING); doc = Jsoup.parse(html); } if (doc.select(".nodata").size() > 0) { return new SearchRequestResult(new ArrayList<SearchResult>(), 0, 1, 1); } doc.setBaseUri(opac_url + "/searchfoo"); int results_total = -1; String resultnumstr = doc.select(".box-header h2").first().text(); if (resultnumstr.contains("(1/1)") || resultnumstr.contains(" 1/1")) { throw new SingleResultFound(); } else if (resultnumstr.contains("(")) { results_total = Integer.parseInt(resultnumstr.replaceAll( ".*\\(([0-9]+)\\).*", "$1")); } else if (resultnumstr.contains(": ")) { results_total = Integer.parseInt(resultnumstr.replaceAll( ".*: ([0-9]+)$", "$1")); } Elements table = doc.select("table.data > tbody > tr"); identifier = null; Elements links = doc.select("table.data a"); boolean haslink = false; for (Element node : links) { if (node.hasAttr("href") & node.attr("href").contains("singleHit.do") && !haslink) { haslink = true; try { List<NameValuePair> anyurl = URLEncodedUtils.parse( new URI(node.attr("href").replace(" ", "%20") .replace("&", "&")), ENCODING); for (NameValuePair nv : anyurl) { if (nv.getName().equals("identifier")) { identifier = nv.getValue(); break; } } } catch (Exception e) { e.printStackTrace(); } } } List<SearchResult> results = new ArrayList<>(); for (int i = 0; i < table.size(); i++) { Element tr = table.get(i); SearchResult sr = new SearchResult(); if (tr.select(".icn, img[width=32]").size() > 0) { String[] fparts = tr.select(".icn, img[width=32]").first() .attr("src").split("/"); String fname = fparts[fparts.length - 1]; String changedFname = fname.toLowerCase(Locale.GERMAN) .replace(".jpg", "").replace(".gif", "") .replace(".png", ""); // File names can look like this: "20_DVD_Video.gif" Pattern pattern = Pattern.compile("(\\d+)_.*"); Matcher matcher = pattern.matcher(changedFname); if (matcher.find()) { changedFname = matcher.group(1); } MediaType defaulttype = defaulttypes.get(changedFname); if (data.has("mediatypes")) { try { sr.setType(MediaType.valueOf(data.getJSONObject( "mediatypes").getString(fname))); } catch (JSONException | IllegalArgumentException e) { sr.setType(defaulttype); } } else { sr.setType(defaulttype); } } String title; String text; if (tr.select(".results table").size() > 0) { // e.g. RWTH Aachen title = tr.select(".title a").text(); text = tr.select(".title div").text(); } else { // e.g. Schaffhausen, BSB München title = tr.select(".title, .hitlistTitle").text(); text = tr.select(".results, .hitlistMetadata").first() .ownText(); } // we need to do some evil javascript parsing here to get the cover // and loan status of the item // get cover if (tr.select(".cover script").size() > 0) { String js = tr.select(".cover script").first().html(); String isbn = matchJSVariable(js, "isbn"); String ajaxUrl = matchJSVariable(js, "ajaxUrl"); if (!"".equals(isbn) && !"".equals(ajaxUrl)) { String url = new URL(new URL(opac_url + "/"), ajaxUrl) .toString(); String coverUrl = httpGet(url + "?isbn=" + isbn + "&size=small", ENCODING); if (!"".equals(coverUrl)) { sr.setCover(coverUrl.replace("\r\n", "").trim()); } } } // get loan status and media ID if (tr.select("div[id^=loanstatus] + script").size() > 0) { String js = tr.select("div[id^=loanstatus] + script").first() .html(); String[] variables = new String[]{"loanstateDBId", "itemIdentifier", "hitlistIdentifier", "hitlistPosition", "duplicateHitlistIdentifier", "itemType", "titleStatus", "typeofHit", "context"}; String ajaxUrl = matchJSVariable(js, "ajaxUrl"); if (!"".equals(ajaxUrl)) { JSONObject id = new JSONObject(); List<NameValuePair> map = new ArrayList<>(); for (String variable : variables) { String value = matchJSVariable(js, variable); if (!"".equals(value)) { map.add(new BasicNameValuePair(variable, value)); } try { if (variable.equals("itemIdentifier")) { id.put("id", value); } else if (variable.equals("loanstateDBId")) { id.put("db", value); } } catch (JSONException e) { e.printStackTrace(); } } sr.setId(id.toString()); String url = new URL(new URL(opac_url + "/"), ajaxUrl) .toString(); String loanStatusHtml = httpGet( url + "?" + URLEncodedUtils.format(map, "UTF-8"), ENCODING).replace("\r\n", "").trim(); Document loanStatusDoc = Jsoup.parse(loanStatusHtml); String loanstatus = loanStatusDoc.text() .replace("\u00bb", "").trim(); if ((loanstatus.startsWith("entliehen") && loanstatus.contains("keine Vormerkung möglich") || loanstatus .contains("Keine Exemplare verfügbar"))) { sr.setStatus(SearchResult.Status.RED); } else if (loanstatus.startsWith("entliehen") || loanstatus.contains("andere Zweigstelle")) { sr.setStatus(SearchResult.Status.YELLOW); } else if ((loanstatus.startsWith("bestellbar") && !loanstatus .contains("nicht bestellbar")) || (loanstatus.startsWith("vorbestellbar") && !loanstatus .contains("nicht vorbestellbar")) || (loanstatus.startsWith("vorbestellbar") && !loanstatus .contains("nicht vorbestellbar")) || (loanstatus.startsWith("vormerkbar") && !loanstatus .contains("nicht vormerkbar")) || (loanstatus.contains("heute zurückgebucht")) || (loanstatus.contains("ausleihbar") && !loanstatus .contains("nicht ausleihbar"))) { sr.setStatus(SearchResult.Status.GREEN); } else if (loanstatus.equals("")) { // In special databases (like "Handschriften" in Winterthur) ID lookup is // not possible, which we try to detect this way. We therefore also cannot // use getResultById when accessing the results. sr.setId(null); } if (sr.getType() != null) { if (sr.getType().equals(MediaType.EBOOK) || sr.getType().equals(MediaType.EVIDEO) || sr.getType().equals(MediaType.MP3)) // Especially Onleihe.de ebooks are often marked // green though they are not available. { sr.setStatus(SearchResult.Status.UNKNOWN); } } } } sr.setInnerhtml(("<b>" + title + "</b><br/>") + text); sr.setNr(10 * (page - 1) + i + 1); results.add(sr); } resultcount = results.size(); return new SearchRequestResult(results, results_total, page); } private String matchJSVariable(String js, String varName) { Pattern patternVar = Pattern.compile("var \\s*" + varName + "\\s*=\\s*\"([^\"]*)\"\\s*;"); Matcher matcher = patternVar.matcher(js); if (matcher.find()) { return matcher.group(1); } else { return null; } } private String matchJSParameter(String js, String varName) { Pattern patternParam = Pattern.compile(".*\\s*" + varName + "\\s*:\\s*('|\")([^\"']*)('|\")\\s*,?.*"); Matcher matcher = patternParam.matcher(js); if (matcher.find()) { return matcher.group(2); } else { return null; } } private String matchHTMLAttr(String js, String varName) { Pattern patternParam = Pattern.compile(".*" + varName + "=('|\")([^\"']*)('|\")\\s*,?.*"); Matcher matcher = patternParam.matcher(js); if (matcher.find()) { return matcher.group(2); } else { return null; } } @Override public DetailedItem getResultById(String id, String homebranch) throws IOException { String html = httpGet(getUrlForId(id), ENCODING); return parse_result(html); } public String getUrlForId(String id) throws UnsupportedEncodingException { try { JSONObject json = new JSONObject(id); if (json.has("url")) { URI permaUrl = new URI(json.getString("url")); URI baseUrl = new URI(opac_url); URI newUrl = new URI(baseUrl.getScheme(), baseUrl.getUserInfo(), baseUrl.getHost(), baseUrl.getPort(), permaUrl.getPath(), permaUrl.getQuery(), permaUrl.getFragment()); return newUrl.toString(); } else { String param = json.optString("field", "0") + "=\"" + json.getString("id") + "\" IN [" + json.getString("db") + "]"; return opac_url + "/perma.do?q=" + URLEncoder.encode(param, "UTF-8"); } } catch (JSONException e) { // backwards compatibility return opac_url + "/perma.do?q=" + URLEncoder.encode("0=\"" + id + "\" IN [2]", "UTF-8"); } catch (URISyntaxException e) { e.printStackTrace(); return null; } } @Override public DetailedItem getResult(int nr) throws IOException { String html = httpGet(opac_url + "/singleHit.do?methodToCall=showHit&curPos=" + nr + "&identifier=" + identifier, ENCODING); return parse_result(html); } protected DetailedItem parse_result(String html) throws IOException { Document doc = Jsoup.parse(html); doc.setBaseUri(opac_url); DetailedItem result = new DetailedItem(); if (doc.select("#cover script").size() > 0) { String js = doc.select("#cover script").first().html(); String isbn = matchJSVariable(js, "isbn"); String ajaxUrl = matchJSVariable(js, "ajaxUrl"); if (ajaxUrl == null) { ajaxUrl = matchJSParameter(js, "url"); } if (ajaxUrl != null && !"".equals(ajaxUrl)) { if (!"".equals(isbn) && isbn != null) { String url = new URL(new URL(opac_url + "/"), ajaxUrl) .toString(); String coverUrl = httpGet(url + "?isbn=" + isbn + "&size=medium", ENCODING); if (!"".equals(coverUrl)) { result.setCover(coverUrl.replace("\r\n", "").trim()); } } else { String url = new URL(new URL(opac_url + "/"), ajaxUrl) .toString(); String coverJs = httpGet(url, ENCODING); result.setCover(matchHTMLAttr(coverJs, "src")); } } } result.setTitle(doc.select("h1").first().text()); if (doc.select("#permalink-link").size() > 0) { String href = doc.select("#permalink-link").first().attr("href"); JSONObject id = new JSONObject(); try { id.put("url", href); result.setId(id.toString()); } catch (JSONException e) { e.printStackTrace(); } } for (Element tr : doc.select(".titleinfo tr")) { // Sometimes there is one th and one td, sometimes two tds String detailName = tr.select("th, td").first().text().trim(); String detailValue = tr.select("td").last().text().trim(); result.addDetail(new Detail(detailName, detailValue)); if (detailName.contains("ID in diesem Katalog") && result.getId() == null) { result.setId(detailValue); } } if (result.getDetails().size() == 0 && doc.select("#details").size() > 0) { // e.g. Bayreuth_Uni String dname = ""; String dval = ""; boolean in_value = true; for (Node n : doc.select("#details").first().childNodes()) { if (n instanceof Element && ((Element) n).tagName().equals("strong")) { if (in_value) { if (dname.length() > 0 && dval.length() > 0) { result.addDetail(new Detail(dname, dval)); } dname = ((Element) n).text(); in_value = false; } else { dname += ((Element) n).text(); } } else { String t = null; if (n instanceof TextNode) { t = ((TextNode) n).text(); } else if (n instanceof Element) { t = ((Element) n).text(); } if (t != null) { if (in_value) { dval += t; } else { in_value = true; dval = t; } } } } } // Copies String copiesParameter = doc.select("div[id^=ajax_holdings_url") .attr("ajaxParameter").replace("&", ""); if (!"".equals(copiesParameter)) { String copiesHtml = httpGet(opac_url + "/" + copiesParameter, ENCODING); Document copiesDoc = Jsoup.parse(copiesHtml); List<String> table_keys = new ArrayList<>(); for (Element th : copiesDoc.select(".data tr th")) { if (th.text().contains("Zweigstelle")) { table_keys.add("branch"); } else if (th.text().contains("Status")) { table_keys.add("status"); } else if (th.text().contains("Signatur")) { table_keys.add("signature"); } else { table_keys.add(null); } } for (Element tr : copiesDoc.select(".data tr:has(td)")) { Copy copy = new Copy(); int i = 0; for (Element td : tr.select("td")) { if (table_keys.get(i) != null) { copy.set(table_keys.get(i), td.text().trim()); } i++; } result.addCopy(copy); } } // Reservation Info, only works if the code above could find a URL if (!"".equals(copiesParameter)) { String reservationParameter = copiesParameter.replace( "showHoldings", "showDocument"); try { String reservationHtml = httpGet(opac_url + "/" + reservationParameter, ENCODING); Document reservationDoc = Jsoup.parse(reservationHtml); reservationDoc.setBaseUri(opac_url); if (reservationDoc.select("a[href*=requestItem.do]").size() == 1) { result.setReservable(true); result.setReservation_info(reservationDoc.select("a") .first().attr("abs:href")); } } catch (Exception e) { e.printStackTrace(); // fail silently } } // TODO: Volumes try { Element isvolume = null; Map<String, String> volume = new HashMap<>(); Elements links = doc.select(".data td a"); int elcount = links.size(); for (int eli = 0; eli < elcount; eli++) { List<NameValuePair> anyurl = URLEncodedUtils.parse(new URI( links.get(eli).attr("href")), "UTF-8"); for (NameValuePair nv : anyurl) { if (nv.getName().equals("methodToCall") && nv.getValue().equals("volumeSearch")) { isvolume = links.get(eli); } else if (nv.getName().equals("catKey")) { volume.put("catKey", nv.getValue()); } else if (nv.getName().equals("dbIdentifier")) { volume.put("dbIdentifier", nv.getValue()); } } if (isvolume != null) { volume.put("volume", "true"); result.setVolumesearch(volume); break; } } } catch (Exception e) { e.printStackTrace(); } return result; } @Override public ReservationResult reservation(DetailedItem item, Account acc, int useraction, String selection) throws IOException { // Earlier, this place used some logic to find out whether it needed to re-login or not // before starting the reservation. Because this didn't work, it now simply logs in every // time. try { login(acc); } catch (OpacErrorException e) { return new ReservationResult(MultiStepResult.Status.ERROR, e.getMessage()); } String html; if (reusehtml_reservation != null) { html = reusehtml_reservation; } else { html = httpGet(item.getReservation_info(), ENCODING); } Document doc = Jsoup.parse(html); if (doc.select(".message-error").size() > 0) { return new ReservationResult(MultiStepResult.Status.ERROR, doc .select(".message-error").first().text()); } List<NameValuePair> nameValuePairs = new ArrayList<>(); nameValuePairs .add(new BasicNameValuePair("methodToCall", "requestItem")); if (doc.select("#newNeedBeforeDate").size() > 0) { nameValuePairs.add(new BasicNameValuePair("newNeedBeforeDate", doc .select("#newNeedBeforeDate").val())); } if (doc.select("select[name=location] option").size() > 0 && selection == null) { Elements options = doc.select("select[name=location] option"); ReservationResult res = new ReservationResult( MultiStepResult.Status.SELECTION_NEEDED); List<Map<String, String>> optionsMap = new ArrayList<>(); for (Element option : options) { Map<String, String> selopt = new HashMap<>(); selopt.put("key", option.attr("value")); selopt.put("value", option.text()); optionsMap.add(selopt); } res.setSelection(optionsMap); res.setMessage(doc.select("label[for=location]").text()); reusehtml_reservation = html; return res; } else if (selection != null) { nameValuePairs.add(new BasicNameValuePair("location", selection)); reusehtml_reservation = null; } nameValuePairs.add(new BasicNameValuePair("submited", "true")); // sic! html = httpPost(opac_url + "/requestItem.do", new UrlEncodedFormEntity( nameValuePairs), ENCODING); doc = Jsoup.parse(html); if (doc.select(".message-confirm").size() > 0) { return new ReservationResult(MultiStepResult.Status.OK); } else if (doc.select(".alert").size() > 0) { return new ReservationResult(MultiStepResult.Status.ERROR, doc .select(".alert").text()); } else { return new ReservationResult(MultiStepResult.Status.ERROR); } } @Override public ProlongResult prolong(String a, Account account, int useraction, String Selection) throws IOException { if (!initialised) { start(); } if (System.currentTimeMillis() - logged_in > SESSION_LIFETIME || logged_in_as == null) { try { account(account); } catch (JSONException e) { e.printStackTrace(); return new ProlongResult(MultiStepResult.Status.ERROR); } catch (OpacErrorException e) { return new ProlongResult(MultiStepResult.Status.ERROR, e.getMessage()); } } else if (logged_in_as.getId() != account.getId()) { try { account(account); } catch (JSONException e) { e.printStackTrace(); return new ProlongResult(MultiStepResult.Status.ERROR); } catch (OpacErrorException e) { return new ProlongResult(MultiStepResult.Status.ERROR, e.getMessage()); } } // We have to call the page we found the link originally on first httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&accountTyp=loaned", ENCODING); // TODO: Check that the right media is prolonged (the links are // index-based and the sorting could change) String html = httpGet(opac_url + "/renewal.do?" + a, ENCODING); Document doc = Jsoup.parse(html); if (doc.select(".message-confirm").size() > 0) { return new ProlongResult(MultiStepResult.Status.OK); } else if (doc.select(".alert").size() > 0) { return new ProlongResult(MultiStepResult.Status.ERROR, doc .select(".alert").first().text()); } else { return new ProlongResult(MultiStepResult.Status.ERROR); } } @Override public CancelResult cancel(String media, Account account, int useraction, String selection) throws IOException, OpacErrorException { if (!initialised) { start(); } if (System.currentTimeMillis() - logged_in > SESSION_LIFETIME || logged_in_as == null) { try { account(account); } catch (JSONException e) { e.printStackTrace(); return new CancelResult(MultiStepResult.Status.ERROR); } catch (OpacErrorException e) { return new CancelResult(MultiStepResult.Status.ERROR, e.getMessage()); } } else if (logged_in_as.getId() != account.getId()) { try { account(account); } catch (JSONException e) { e.printStackTrace(); return new CancelResult(MultiStepResult.Status.ERROR); } catch (OpacErrorException e) { return new CancelResult(MultiStepResult.Status.ERROR, e.getMessage()); } } // We have to call the page we found the link originally on first httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&accountTyp=requested", ENCODING); // TODO: Check that the right media is prolonged (the links are // index-based and the sorting could change) String html = httpGet(opac_url + "/cancelReservation.do?" + media, ENCODING); Document doc = Jsoup.parse(html); if (doc.select(".message-confirm").size() > 0) { return new CancelResult(MultiStepResult.Status.OK); } else if (doc.select(".alert").size() > 0) { return new CancelResult(MultiStepResult.Status.ERROR, doc .select(".alert").first().text()); } else { return new CancelResult(MultiStepResult.Status.ERROR); } } @Override public AccountData account(Account acc) throws IOException, JSONException, OpacErrorException { start(); LoginResponse login = login(acc); if (!login.success) { return null; } AccountData adata = new AccountData(acc.getId()); if (login.warning != null) { adata.setWarning(login.warning); } // Lent media httpGet(opac_url + "/userAccount.do?methodToCall=start", ENCODING); String html = httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&accountTyp=loaned", ENCODING); List<LentItem> lent = new ArrayList<>(); Document doc = Jsoup.parse(html); doc.setBaseUri(opac_url); List<LentItem> nextpageLent = parse_medialist(doc); if (nextpageLent != null) { lent.addAll(nextpageLent); } if (doc.select(".pagination").size() > 0 && lent != null) { Element pagination = doc.select(".pagination").first(); Elements pages = pagination.select("a"); for (Element page : pages) { if (!page.hasAttr("href")) { continue; } html = httpGet(page.attr("abs:href"), ENCODING); doc = Jsoup.parse(html); doc.setBaseUri(opac_url); nextpageLent = parse_medialist(doc); if (nextpageLent != null) { lent.addAll(nextpageLent); } } } adata.setLent(lent); // Requested media ("Vormerkungen") html = httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&accountTyp=requested", ENCODING); doc = Jsoup.parse(html); doc.setBaseUri(opac_url); List<ReservedItem> requested = new ArrayList<>(); List<ReservedItem> nextpageRes = parse_reslist(doc); if (nextpageRes != null) { requested.addAll(nextpageRes); } if (doc.select(".pagination").size() > 0 && requested != null) { Element pagination = doc.select(".pagination").first(); Elements pages = pagination.select("a"); for (Element page : pages) { if (!page.hasAttr("href")) { continue; } html = httpGet(page.attr("abs:href"), ENCODING); doc = Jsoup.parse(html); doc.setBaseUri(opac_url); nextpageRes = parse_reslist(doc); if (nextpageRes != null) { requested.addAll(nextpageRes); } } } // Ordered media ("Bestellungen") html = httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&accountTyp=ordered", ENCODING); doc = Jsoup.parse(html); doc.setBaseUri(opac_url); List<ReservedItem> nextpageOrd = parse_reslist(doc); if (nextpageOrd != null) { requested.addAll(nextpageOrd); } if (doc.select(".pagination").size() > 0 && requested != null) { Element pagination = doc.select(".pagination").first(); Elements pages = pagination.select("a"); for (Element page : pages) { if (!page.hasAttr("href")) { continue; } html = httpGet(page.attr("abs:href"), ENCODING); doc = Jsoup.parse(html); doc.setBaseUri(opac_url); nextpageOrd = parse_reslist(doc); if (nextpageOrd != null) { requested.addAll(nextpageOrd); } } } adata.setReservations(requested); // Fees if (doc.select("#fees").size() > 0) { String text = doc.select("#fees").first().text().trim(); if (text.matches("Geb.+hren[^\\(]+\\(([0-9.,]+)[^0-9€A-Z]*(€|EUR|CHF|Fr)\\)")) { text = text .replaceAll( "Geb.+hren[^\\(]+\\(([0-9.,]+)[^0-9€A-Z]*(€|EUR|CHF|Fr)\\)", "$1 $2"); adata.setPendingFees(text); } } return adata; } static List<LentItem> parse_medialist(Document doc) { List<LentItem> media = new ArrayList<>(); Elements copytrs = doc.select(".data tr"); DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); int trs = copytrs.size(); if (trs == 1) { return null; } assert (trs > 0); for (int i = 1; i < trs; i++) { Element tr = copytrs.get(i); LentItem item = new LentItem(); if (tr.text().contains("keine Daten")) { return null; } item.setTitle(tr.select(".account-display-title").select("b, strong") .text().trim()); try { item.setRenewable(false); if (tr.select("a").size() > 0) { for (Element link : tr.select("a")) { String href = link.attr("abs:href"); Map<String, String> hrefq = getQueryParamsFirst(href); if (hrefq.containsKey("q")) { item.setId(extractIdFromQ(hrefq.get("q"))); } else if ("renewal".equals(hrefq.get("methodToCall"))) { item.setProlongData(href.split("\\?")[1]); item.setRenewable(true); link.remove(); break; } } } String[] lines = tr.select(".account-display-title").html().split("<br[ /]*>"); if (lines.length == 4 || lines.length == 5) { // Winterthur item.setAuthor(Jsoup.parse(lines[1]).text().trim()); item.setBarcode(Jsoup.parse(lines[2]).text().trim()); if (lines.length == 5) { // Chemnitz item.setStatus(Jsoup.parse(lines[3] + " " + lines[4]).text().trim()); } else { // Winterthur item.setStatus(Jsoup.parse(lines[3]).text().trim()); } } else if (lines.length == 3) { // We can't really tell the difference between missing author and missing // shelfmark. However, all items have shelfmarks, not all have authors. item.setBarcode(Parser.unescapeEntities(lines[1].trim(), false)); item.setStatus(Parser.unescapeEntities(lines[2].trim(), false)); } else if (lines.length == 2) { item.setAuthor(Parser.unescapeEntities(lines[1].trim(), false)); } String[] col3split = tr.select(".account-display-state").html().split("<br[ /]*>"); String deadline = Jsoup.parse(col3split[0].trim()).text().trim(); if (deadline.contains(":")) { // BSB Munich: <span class="hidden-sm hidden-md hidden-lg">Fälligkeitsdatum : // </span>26.02.2016<br> deadline = deadline.split(":")[1].trim(); } if (deadline.contains("-")) { // Chemnitz: 22.07.2015 - 20.10.2015<br> deadline = deadline.split("-")[1].trim(); } try { item.setDeadline(fmt.parseLocalDate(deadline).toString()); } catch (IllegalArgumentException e1) { e1.printStackTrace(); } if (col3split.length > 1) item.setHomeBranch(col3split[1].trim()); } catch (Exception ex) { ex.printStackTrace(); } media.add(item); } return media; } private static String extractIdFromQ(String q) { Pattern pattern = Pattern.compile("(\\d+)=\"(?:\\\\\")?([^\\\\]+)(?:\\\\\")?\" IN \\[" + "(\\d+)\\]"); Matcher matcher = pattern.matcher(q); if (matcher.find()) { JSONObject id = new JSONObject(); try { id.put("field", matcher.group(1)); id.put("id", matcher.group(2)); id.put("db", matcher.group(3)); return id.toString(); } catch (JSONException e) { e.printStackTrace(); return null; } } else { return null; } } static List<ReservedItem> parse_reslist(Document doc) { List<ReservedItem> reservations = new ArrayList<>(); Elements copytrs = doc.select(".data tr, #account-data .table tr"); int trs = copytrs.size(); if (trs <= 1) { return null; } for (int i = 1; i < trs; i++) { Element tr = copytrs.get(i); ReservedItem item = new ReservedItem(); if (tr.text().contains("keine Daten") || tr.children().size() == 1) { return null; } item.setTitle(tr.child(2).select("b, strong").text().trim()); try { String[] rowsplit2 = tr.child(2).html().split("<br[ /]*>"); String[] rowsplit3 = tr.child(3).html().split("<br[ /]*>"); if (rowsplit2.length > 1) item.setAuthor(rowsplit2[1].replace("</a>", "").trim()); if (rowsplit3.length > 2) item.setBranch(rowsplit3[2].replace("</a>", "").trim()); if (rowsplit3.length > 2) { item.setStatus(rowsplit3[0].trim() + " (" + rowsplit3[1].trim() + ")"); } if (tr.select("a").size() > 0) { for (Element link : tr.select("a")) { String href = link.attr("abs:href"); Map<String, String> hrefq = getQueryParamsFirst(href); if (hrefq.containsKey("q")) { item.setId(extractIdFromQ(hrefq.get("q"))); } else if ("cancel".equals(hrefq.get("methodToCall"))) { item.setCancelData(href.split("\\?")[1]); break; } } } } catch (Exception e) { e.printStackTrace(); } reservations.add(item); } return reservations; } protected LoginResponse login(Account acc) throws OpacErrorException, IOException { String html; List<NameValuePair> nameValuePairs = new ArrayList<>(); try { httpGet(opac_url + "/login.do", ENCODING); } catch (IOException e1) { e1.printStackTrace(); } nameValuePairs.add(new BasicNameValuePair("username", acc.getName())); nameValuePairs .add(new BasicNameValuePair("password", acc.getPassword())); nameValuePairs.add(new BasicNameValuePair("CSId", CSId)); nameValuePairs.add(new BasicNameValuePair("methodToCall", "submit")); nameValuePairs.add(new BasicNameValuePair("login_action", "Login")); html = httpPost(opac_url + "/login.do", new UrlEncodedFormEntity( nameValuePairs), ENCODING); Document doc = Jsoup.parse(html); if (doc.getElementsByClass("alert").size() > 0) { if (doc.select(".alert").text().contains("Nutzungseinschr") && doc.select("a[href*=methodToCall=done]").size() > 0) { // This is a warning that we need to acknowledge, it will be shown in the account // view httpGet(opac_url + "/login.do?methodToCall=done", ENCODING); logged_in = System.currentTimeMillis(); logged_in_as = acc; return new LoginResponse(true, doc.getElementsByClass("alert").get(0).text()); } else { throw new OpacErrorException(doc.getElementsByClass("alert").get(0).text()); } } logged_in = System.currentTimeMillis(); logged_in_as = acc; return new LoginResponse(true); } @Override public String getShareUrl(String id, String title) { try { return getUrlForId(id); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } } @Override public int getSupportFlags() { int flags = SUPPORT_FLAG_CHANGE_ACCOUNT | SUPPORT_FLAG_ACCOUNT_PROLONG_ALL; flags |= SUPPORT_FLAG_ENDLESS_SCROLLING; return flags; } @Override public ProlongAllResult prolongAll(Account account, int useraction, String selection) throws IOException { if (!initialised) { start(); } if (System.currentTimeMillis() - logged_in > SESSION_LIFETIME || logged_in_as == null || logged_in_as.getId() != account.getId()) { try { login(account); } catch (OpacErrorException e) { return new ProlongAllResult(MultiStepResult.Status.ERROR, e.getMessage()); } } // We have to call the page we found the link originally on first httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&accountTyp=loaned", ENCODING); String html = httpGet(opac_url + "/renewal.do?methodToCall=accountRenewal", ENCODING); Document doc = Jsoup.parse(html); if (doc.select(".message-confirm, .message-info").size() > 0) { return new ProlongAllResult(MultiStepResult.Status.OK, doc.select(".message-info").first().text()); } else if (doc.select(".alert").size() > 0) { return new ProlongAllResult(MultiStepResult.Status.ERROR, doc .select(".alert").first().text()); } else { return new ProlongAllResult(MultiStepResult.Status.ERROR); } } @Override public SearchRequestResult filterResults(Filter filter, Option option) throws IOException, OpacErrorException { // TODO Auto-generated method stub return null; } @Override public void checkAccountData(Account account) throws IOException, JSONException, OpacErrorException { if (!login(account).success) { throw new OpacErrorException(stringProvider.getString(StringProvider.LOGIN_FAILED)); } } @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; } private class LoginResponse { public boolean success; public String warning; public LoginResponse(boolean success) { this.success = success; } public LoginResponse(boolean success, String warning) { this.success = success; this.warning = warning; } } }