/** * Copyright (C) 2013 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.select.Elements; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.ParseException; import java.text.SimpleDateFormat; 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.objects.SearchResult.Status; import de.geeksfactory.opacclient.searchfields.DropdownSearchField; import de.geeksfactory.opacclient.searchfields.SearchField; import de.geeksfactory.opacclient.searchfields.SearchQuery; import de.geeksfactory.opacclient.searchfields.TextSearchField; /** * Implementation of Fleischmann iOpac, including account support Seems to work in all the libraries * currently supported without any modifications. * * @author Johan von Forstner, 17.09.2013 */ public class IOpac extends BaseApi implements OpacApi { protected static HashMap<String, MediaType> defaulttypes = new HashMap<>(); static { defaulttypes.put("b", MediaType.BOOK); defaulttypes.put("o", MediaType.BOOK); defaulttypes.put("e", MediaType.BOOK); defaulttypes.put("p", MediaType.BOOK); defaulttypes.put("j", MediaType.BOOK); defaulttypes.put("g", MediaType.BOOK); defaulttypes.put("k", MediaType.BOOK); defaulttypes.put("a", MediaType.BOOK); defaulttypes.put("c", MediaType.AUDIOBOOK); defaulttypes.put("u", MediaType.AUDIOBOOK); defaulttypes.put("l", MediaType.AUDIOBOOK); defaulttypes.put("q", MediaType.CD_SOFTWARE); defaulttypes.put("r", MediaType.CD_SOFTWARE); defaulttypes.put("v", MediaType.MOVIE); defaulttypes.put("d", MediaType.CD_MUSIC); defaulttypes.put("n", MediaType.SCORE_MUSIC); defaulttypes.put("s", MediaType.BOARDGAME); defaulttypes.put("z", MediaType.MAGAZINE); defaulttypes.put("x", MediaType.MAGAZINE); } protected String opac_url = ""; protected String dir = "/iopac"; protected JSONObject data; protected String reusehtml; protected String rechnr; protected int results_total; protected boolean newShareLinks; @Override public void init(Library lib, HttpClientFactory httpClientFactory) { super.init(lib, httpClientFactory); this.data = lib.getData(); try { this.opac_url = data.getString("baseurl"); if (data.has("dir")) { this.dir = data.getString("dir"); } } catch (JSONException e) { throw new RuntimeException(e); } } protected int addParameters(SearchQuery query, List<NameValuePair> params, int index) { if (query.getValue().equals("")) { return index; } params.add(new BasicNameValuePair(query.getKey(), query.getValue())); return index + 1; } @Override public SearchRequestResult search(List<SearchQuery> queries) throws IOException, OpacErrorException { if (!initialised) { start(); } List<NameValuePair> params = new ArrayList<>(); int index = 0; start(); for (SearchQuery query : queries) { index = addParameters(query, params, index); } params.add(new BasicNameValuePair("Anzahl", "10")); params.add(new BasicNameValuePair("pshStart", "Suchen")); if (index == 0) { throw new OpacErrorException( stringProvider.getString(StringProvider.NO_CRITERIA_INPUT)); } String html = httpPost(opac_url + "/cgi-bin/di.exe", new UrlEncodedFormEntity(params, "iso-8859-1"), getDefaultEncoding()); return parse_search(html, 1); } protected SearchRequestResult parse_search(String html, int page) throws OpacErrorException, NotReachableException { Document doc = Jsoup.parse(html); if (doc.select("h4").size() > 0) { if (doc.select("h4").text().trim().startsWith("0 gefundene Medien")) { // nothing found return new SearchRequestResult(new ArrayList<SearchResult>(), 0, 1, 1); } else if (!doc.select("h4").text().trim() .contains("gefundene Medien") && !doc.select("h4").text().trim() .contains("Es wurden mehr als")) { // error throw new OpacErrorException(doc.select("h4").text().trim()); } } else if (doc.select("h1").size() > 0) { if (doc.select("h1").text().trim().contains("RUNTIME ERROR")) { // Server Error throw new NotReachableException("IOPAC RUNTIME ERROR"); } else { throw new OpacErrorException(stringProvider.getFormattedString( StringProvider.UNKNOWN_ERROR_WITH_DESCRIPTION, doc .select("h1").text().trim())); } } else { return null; } updateRechnr(doc); reusehtml = html; results_total = -1; if (doc.select("h4").text().trim().contains("Es wurden mehr als")) { results_total = 200; } else { String resultnumstr = doc.select("h4").first().text(); resultnumstr = resultnumstr.substring(0, resultnumstr.indexOf(" ")) .trim(); results_total = Integer.parseInt(resultnumstr); } List<SearchResult> results = new ArrayList<>(); Elements tables = doc.select("table").first().select("tr:has(td)"); Map<String, Integer> colmap = new HashMap<>(); Element thead = doc.select("table").first().select("tr:has(th)") .first(); int j = 0; for (Element th : thead.select("th")) { String text = th.text().trim().toLowerCase(Locale.GERMAN); if (text.contains("cover")) { colmap.put("cover", j); } else if (text.contains("titel")) { colmap.put("title", j); } else if (text.contains("verfasser")) { colmap.put("author", j); } else if (text.contains("mtyp")) { colmap.put("category", j); } else if (text.contains("jahr")) { colmap.put("year", j); } else if (text.contains("signatur")) { colmap.put("shelfmark", j); } else if (text.contains("info")) { colmap.put("info", j); } else if (text.contains("abteilung")) { colmap.put("department", j); } else if (text.contains("verliehen") || text.contains("verl.")) { colmap.put("returndate", j); } else if (text.contains("anz.res")) { colmap.put("reservations", j); } j++; } if (colmap.size() == 0) { colmap.put("cover", 0); colmap.put("title", 1); colmap.put("author", 2); colmap.put("publisher", 3); colmap.put("year", 4); colmap.put("department", 5); colmap.put("shelfmark", 6); colmap.put("returndate", 7); colmap.put("category", 8); } for (int i = 0; i < tables.size(); i++) { Element tr = tables.get(i); SearchResult sr = new SearchResult(); if (tr.select("td").get(colmap.get("cover")).select("img").size() > 0) { String imgUrl = tr.select("td").get(colmap.get("cover")) .select("img").first().attr("src"); sr.setCover(imgUrl); } // Media Type if (colmap.get("category") != null) { String mType = tr.select("td").get(colmap.get("category")) .text().trim().replace("\u00a0", ""); if (data.has("mediatypes")) { try { sr.setType(MediaType.valueOf(data.getJSONObject( "mediatypes").getString( mType.toLowerCase(Locale.GERMAN)))); } catch (JSONException | IllegalArgumentException e) { sr.setType(defaulttypes.get(mType .toLowerCase(Locale.GERMAN))); } } else { sr.setType(defaulttypes.get(mType .toLowerCase(Locale.GERMAN))); } } // Title and additional info String title; String additionalInfo = ""; if (colmap.get("info") != null) { Element info = tr.select("td").get(colmap.get("info")); title = info.select("a[title=Details-Info]").text().trim(); String authorIn = info.text().substring(0, info.text().indexOf(title)); if (authorIn.contains(":")) { authorIn = authorIn.replaceFirst("^([^:]*):(.*)$", "$1"); additionalInfo += " - " + authorIn; } } else { title = tr.select("td").get(colmap.get("title")).text().trim() .replace("\u00a0", ""); if (title.contains("(") && title.indexOf("(") > 0) { additionalInfo += title.substring(title.indexOf("(")); title = title.substring(0, title.indexOf("(") - 1).trim(); } // Author if (colmap.containsKey("author")) { String author = tr.select("td").get(colmap.get("author")) .text().trim().replace("\u00a0", ""); additionalInfo += " - " + author; } } // Publisher if (colmap.containsKey("publisher")) { String publisher = tr.select("td").get(colmap.get("publisher")) .text().trim().replace("\u00a0", ""); additionalInfo += " (" + publisher; } // Year if (colmap.containsKey("year")) { String year = tr.select("td").get(colmap.get("year")).text() .trim().replace("\u00a0", ""); additionalInfo += ", " + year + ")"; } sr.setInnerhtml("<b>" + title + "</b><br>" + additionalInfo); // Status String status = tr.select("td").get(colmap.get("returndate")) .text().trim().replace("\u00a0", ""); SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN); try { df.parse(status); // this is a return date sr.setStatus(Status.RED); sr.setInnerhtml(sr.getInnerhtml() + "<br><i>" + stringProvider.getString(StringProvider.LENT_UNTIL) + " " + status + "</i>"); } catch (ParseException e) { // this is a different status text String lc = status.toLowerCase(Locale.GERMAN); if ((lc.equals("") || lc.toLowerCase(Locale.GERMAN).contains("onleihe") || lc.contains("verleihbar") || lc.contains("entleihbar") || lc.contains("ausleihbar")) && !lc.contains("nicht")) { sr.setStatus(Status.GREEN); } else { sr.setStatus(Status.YELLOW); sr.setInnerhtml(sr.getInnerhtml() + "<br><i>" + status + "</i>"); } } // In some libraries (for example search for "atelier" in Preetz) // the results are sorted differently than their numbers suggest, so // we need to detect the number ("recno") from the link String link = tr.select("a[href^=/cgi-bin/di.exe?page=]").attr( "href"); Map<String, String> params = getQueryParamsFirst(link); if (params.containsKey("recno")) { int recno = Integer.valueOf(params.get("recno")); sr.setNr(recno - 1); } else { // the above should work, but fall back to this if it doesn't sr.setNr(10 * (page - 1) + i); } // In some libraries (for example Preetz) we can detect the media ID // here using another link present in the search results Elements idLinks = tr.select("a[href^=/cgi-bin/di.exe?cMedNr]"); if (idLinks.size() > 0) { Map<String, String> idParams = getQueryParamsFirst(idLinks .first().attr("href")); String id = idParams.get("cMedNr"); sr.setId(id); } else { sr.setId(null); } results.add(sr); } return new SearchRequestResult(results, results_total, page); } @Override public SearchRequestResult searchGetPage(int page) throws IOException, OpacErrorException { if (!initialised) { start(); } String html = httpGet(opac_url + "/cgi-bin/di.exe?page=" + page + "&rechnr=" + rechnr + "&Anzahl=10&FilNr=", getDefaultEncoding()); return parse_search(html, page); } @Override public SearchRequestResult filterResults(Filter filter, Option option) throws IOException { return null; } @Override public DetailedItem getResultById(String id, String homebranch) throws IOException { if (!initialised) { start(); } if (id == null && reusehtml != null) { return parse_result(reusehtml); } String html = httpGet(opac_url + "/cgi-bin/di.exe?cMedNr=" + id + "&mode=23", getDefaultEncoding()); return parse_result(html); } @Override public DetailedItem getResult(int position) throws IOException { if (!initialised) { start(); } int page = Double.valueOf(Math.floor(position / 10)).intValue() + 1; String html = httpGet(opac_url + "/cgi-bin/di.exe?page=" + page + "&rechnr=" + rechnr + "&Anzahl=10&recno=" + (position + 1) + "&FilNr=", getDefaultEncoding()); return parse_result(html); } protected DetailedItem parse_result(String html) throws IOException { Document doc = Jsoup.parse(html); DetailedItem result = new DetailedItem(); String id = null; if (doc.select("input[name=mednr]").size() > 0) { id = doc.select("input[name=mednr]").first().val().trim(); } else if(doc.select("a[href*=mednr]").size() > 0) { String href = doc.select("a[href*=mednr]").first().attr("href"); id = getQueryParamsFirst(href).get("mednr").trim(); } result.setId(id); // check if new share button is available (allows to share a link to the standard // frameset of the OPAC instead of only the detail frame) newShareLinks = doc.select("#sharebutton").size() > 0; Elements table = doc.select("table").get(1).select("tr"); // GET COVER IMAGE String imgUrl = table.get(0) .select("img[src~=^https?://(:?images(?:-[^\\.]*)?\\.|[^\\.]*\\" + ".images-)amazon\\.com]") .attr("src"); result.setCover(imgUrl); // GET INFORMATION Copy copy = new Copy(); for (Element element : table) { String detail = element.select("td").text().trim() .replace("\u00a0", ""); String title = element.select("th").text().trim() .replace("\u00a0", ""); if (!title.equals("")) { if (title.contains("verliehen bis")) { if (detail.equals("")) { copy.setStatus("verfügbar"); } else { copy.setStatus("verliehen bis " + detail); } } else if (title.contains("Abteilung")) { copy.setDepartment(detail); } else if (title.contains("Signatur")) { copy.setShelfmark(detail); } else if (title.contains("Titel")) { result.setTitle(detail); } else if (!title.contains("Cover")) { result.addDetail(new Detail(title, detail)); } } } // GET RESERVATION INFO if ("verfügbar".equals(copy.getStatus()) || doc.select("a[href^=/cgi-bin/di.exe?mode=10], input.resbutton").size() == 0) { result.setReservable(false); } else { result.setReservable(true); if (doc.select("a[href^=/cgi-bin/di.exe?mode=10]").size() > 0) { // Reservation via link result.setReservation_info(doc .select("a[href^=/cgi-bin/di.exe?mode=10]").first() .attr("href").substring(1).replace(" ", "")); } else { // Reservation via form (method="get") Element form = doc.select("input.resbutton").first().parent(); result.setReservation_info(generateQuery(form)); } } if (copy.notEmpty()) result.addCopy(copy); return result; } private String generateQuery(Element form) throws UnsupportedEncodingException { StringBuilder builder = new StringBuilder(); builder.append(form.attr("action").substring(1)); int i = 0; for (Element input : form.select("input")) { builder.append(i == 0 ? "?" : "&"); builder.append(input.attr("name")).append("=") .append(URLEncoder.encode(input.attr("value"), "UTF-8")); i++; } return builder.toString(); } @Override public ReservationResult reservation(DetailedItem item, Account account, int useraction, String selection) throws IOException { String reservation_info = item.getReservation_info(); // STEP 1: Login page String html = httpGet(opac_url + "/" + reservation_info, getDefaultEncoding()); Document doc = Jsoup.parse(html); if (doc.select("table").first().text().contains("kann nicht")) { return new ReservationResult(MultiStepResult.Status.ERROR, doc .select("table").first().text().trim()); } if (doc.select("form[name=form1]").size() == 0) { return new ReservationResult(MultiStepResult.Status.ERROR); } Element form = doc.select("form[name=form1]").first(); List<BasicNameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("sleKndNr", account.getName())); params.add(new BasicNameValuePair("slePw", account.getPassword())); params.add(new BasicNameValuePair("pshLogin", "Reservieren")); for (Element input : form.select("input[type=hidden]")) { params.add(new BasicNameValuePair(input.attr("name"), input .attr("value"))); } // STEP 2: Confirmation page html = httpPost(opac_url + "/cgi-bin/di.exe", new UrlEncodedFormEntity( params), getDefaultEncoding()); doc = Jsoup.parse(html); if (doc.select("form[name=form1]").size() > 0) { // STEP 3: There is another confirmation needed form = doc.select("form[name=form1]").first(); html = httpGet(opac_url + "/" + generateQuery(form), getDefaultEncoding()); doc = Jsoup.parse(html); } if (doc.text().contains("fehlgeschlagen") || doc.text().contains("Achtung") || doc.text().contains("nicht m")) { return new ReservationResult(MultiStepResult.Status.ERROR, doc .select("table").first().text().trim()); } else { return new ReservationResult(MultiStepResult.Status.OK); } } @Override public ProlongResult prolong(String media, Account account, int useraction, String Selection) throws IOException { // internal convention: We add "NEW" to the media ID to show that we have the new iOPAC // version if (media.startsWith("NEW")) { String mediaNr = media.substring(3); String html = httpGet(opac_url + "/cgi-bin/di.exe?mode=42&MedNrVerlAll=" + URLEncoder.encode(mediaNr, "UTF-8"), getDefaultEncoding()); Document doc = Jsoup.parse(html); if (doc.text().contains("1 Medium wurde verl")) { return new ProlongResult(MultiStepResult.Status.OK); } else { return new ProlongResult(MultiStepResult.Status.ERROR, doc.text()); } } else { String html = httpGet(opac_url + "/" + media, getDefaultEncoding()); Document doc = Jsoup.parse(html); if (doc.select("table th").size() > 0) { if (doc.select("h1").size() > 0) { if (doc.select("h1").first().text().contains("Hinweis")) { return new ProlongResult(MultiStepResult.Status.ERROR, doc .select("table th").first().text()); } } try { Element form = doc.select("form[name=form1]").first(); String sessionid = form.select("input[name=sessionid]").attr( "value"); String mednr = form.select("input[name=mednr]").attr("value"); httpGet(opac_url + "/cgi-bin/di.exe?mode=8&kndnr=" + account.getName() + "&mednr=" + mednr + "&sessionid=" + sessionid + "&psh100=Verl%C3%A4ngern", getDefaultEncoding()); return new ProlongResult(MultiStepResult.Status.OK); } catch (Throwable e) { e.printStackTrace(); return new ProlongResult(MultiStepResult.Status.ERROR); } } return new ProlongResult(MultiStepResult.Status.ERROR); } } @Override public ProlongAllResult prolongAll(Account account, int useraction, String selection) throws IOException { Document doc = getAccountPage(account); // Check if the iOPAC verion supports this feature if (doc.select("button.verlallbutton").size() > 0) { List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("mode", "42")); for (Element checkbox : doc.select("input.VerlAllCheckboxOK")) { params.add(new BasicNameValuePair("MedNrVerlAll", checkbox.val())); } String html = httpGet( opac_url + "/cgi-bin/di.exe?" + URLEncodedUtils.format(params, "UTF-8"), getDefaultEncoding()); Document doc2 = Jsoup.parse(html); Pattern pattern = Pattern.compile("(\\d+ Medi(?:en|um) wurden? verl.ngert)\\s*(\\d+ " + "Medi(?:en|um) wurden? nicht verl.ngert)?"); Matcher matcher = pattern.matcher(doc2.text()); if (matcher.find()) { String text1 = matcher.group(1); String text2 = matcher.group(2); List<Map<String, String>> list = new ArrayList<>(); Map<String, String> map1 = new HashMap<>(); // TODO: We are abusing the ProlongAllResult.KEY_LINE_ ... keys here because we // do not get information about all the media map1.put(ProlongAllResult.KEY_LINE_TITLE, text1); list.add(map1); if (text2 != null && !text2.equals("")) { Map<String, String> map2 = new HashMap<>(); map2.put(ProlongAllResult.KEY_LINE_TITLE, text2); list.add(map2); } return new ProlongAllResult(MultiStepResult.Status.OK, list); } else { return new ProlongAllResult(MultiStepResult.Status.ERROR, doc2.text()); } } else { return new ProlongAllResult(MultiStepResult.Status.ERROR, stringProvider.getString(StringProvider.UNSUPPORTED_IN_LIBRARY)); } } @Override public CancelResult cancel(String media, Account account, int useraction, String selection) throws IOException, OpacErrorException { String html = httpGet(opac_url + "/" + media, getDefaultEncoding()); Document doc = Jsoup.parse(html); try { Element form = doc.select("form[name=form1]").first(); String sessionid = form.select("input[name=sessionid]").attr( "value"); String kndnr = form.select("input[name=kndnr]").attr("value"); String mednr = form.select("input[name=mednr]").attr("value"); httpGet(opac_url + "/cgi-bin/di.exe?mode=9&kndnr=" + kndnr + "&mednr=" + mednr + "&sessionid=" + sessionid + "&psh100=Stornieren", getDefaultEncoding()); return new CancelResult(MultiStepResult.Status.OK); } catch (Throwable e) { e.printStackTrace(); throw new NotReachableException(e.getMessage()); } } @Override public AccountData account(Account account) throws IOException, JSONException, OpacErrorException { if (!initialised) { start(); } Document doc = getAccountPage(account); AccountData res = new AccountData(account.getId()); List<LentItem> media = new ArrayList<>(); List<ReservedItem> reserved = new ArrayList<>(); parseMediaList(media, doc, data); parseResList(reserved, doc, data); res.setLent(media); res.setReservations(reserved); if (doc.select("h4:contains(Kontostand)").size() > 0) { Element h4 = doc.select("h4:contains(Kontostand)").first(); Pattern regex = Pattern.compile("Kontostand (-?\\d+\\.\\d\\d EUR)"); Matcher matcher = regex.matcher(h4.text()); if (matcher.find()) res.setPendingFees(matcher.group(1)); } if (doc.select("h4:contains(Ausweis g)").size() > 0) { Element h4 = doc.select("h4:contains(Ausweis g)").first(); Pattern regex = Pattern.compile("Ausweis g.+ltig bis\\s*.\\s*(\\d\\d.\\d\\d.\\d\\d\\d\\d)"); Matcher matcher = regex.matcher(h4.text()); if (matcher.find()) res.setValidUntil(matcher.group(1)); } if (media.isEmpty() && reserved.isEmpty()) { if (doc.select("h1").size() > 0) { //noinspection StatementWithEmptyBody if (doc.select("h4").text().trim().contains("keine ausgeliehenen Medien")) { // There is no lent media, but the server is working // correctly } else if (doc.select("h1").text().trim() .contains("RUNTIME ERROR")) { // Server Error throw new NotReachableException("IOPAC RUNTIME ERROR"); } else { throw new OpacErrorException( stringProvider .getFormattedString( StringProvider.UNKNOWN_ERROR_ACCOUNT_WITH_DESCRIPTION, doc.select("h1").text().trim())); } } else { throw new OpacErrorException( stringProvider .getString(StringProvider.UNKNOWN_ERROR_ACCOUNT)); } } return res; } private Document getAccountPage(Account account) throws IOException { List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("sleKndNr", account.getName())); params.add(new BasicNameValuePair("slePw", account.getPassword())); params.add(new BasicNameValuePair("pshLogin", "Login")); String html = httpPost(opac_url + "/cgi-bin/di.exe", new UrlEncodedFormEntity(params, "iso-8859-1"), getDefaultEncoding()); return Jsoup.parse(html); } public void checkAccountData(Account account) throws IOException, OpacErrorException { Document doc = getAccountPage(account); if (doc.select("h1, .HTMLInfo_Head").text().contains("fehlgeschlagen")) { throw new OpacErrorException(doc.select("h1, th, .HTMLInfo_Text").text()); } } static void parseMediaList(List<LentItem> media, Document doc, JSONObject data) { if (doc.select("a[name=AUS]").size() == 0) return; Elements copytrs = doc.select("a[name=AUS] ~ table, a[name=AUS] ~ form table").first() .select("tr"); doc.setBaseUri(data.optString("baseurl")); DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); int trs = copytrs.size(); if (trs < 2) { return; } assert (trs > 0); JSONObject copymap = new JSONObject(); try { if (data.has("accounttable")) { copymap = data.getJSONObject("accounttable"); } } catch (JSONException e) { } Pattern datePattern = Pattern.compile("\\d{2}\\.\\d{2}\\.\\d{4}"); Pattern reservedPattern = Pattern.compile("\\d+ x reserv."); for (int i = 1; i < trs; i++) { Element tr = copytrs.get(i); LentItem item = new LentItem(); if (copymap.optInt("title", 0) >= 0) { item.setTitle( tr.child(copymap.optInt("title", 0)).text().trim().replace("\u00a0", "")); } if (copymap.optInt("author", 1) >= 0) { item.setAuthor( tr.child(copymap.optInt("author", 1)).text().trim().replace("\u00a0", "")); } if (copymap.optInt("format", 2) >= 0) { item.setFormat( tr.child(copymap.optInt("format", 2)).text().trim().replace("\u00a0", "")); } int prolongCount = 0; if (copymap.optInt("prolongcount", 3) >= 0) { prolongCount = Integer.parseInt(tr .child(copymap.optInt("prolongcount", 3)).text().trim() .replace("\u00a0", "")); item.setStatus(String.valueOf(prolongCount) + "x verl."); } if (data.optInt("maxprolongcount", -1) != -1) { item.setRenewable(prolongCount < data.optInt("maxprolongcount", -1)); } if (copymap.optInt("returndate", 4) >= 0) { String value = tr.child(copymap.optInt("returndate", 4)).text().trim() .replace("\u00a0", ""); Matcher matcher = datePattern.matcher(value); if (matcher.find()) { try { item.setDeadline(fmt.parseLocalDate(matcher.group())); } catch (IllegalArgumentException e1) { e1.printStackTrace(); } } matcher = reservedPattern.matcher(value); if (matcher.find()) { if (item.getStatus() != null) { item.setStatus(item.getStatus() + ", " + matcher.group()); } else { item.setStatus(matcher.group()); } } } if (copymap.optInt("prolongurl", 5) >= 0) { if (tr.children().size() > copymap.optInt("prolongurl", 5)) { Element cell = tr.child(copymap.optInt("prolongurl", 5)); if (cell.select("input[name=MedNrVerlAll]").size() > 0) { // new iOPAC Version 1.45 - checkboxes to prolong multiple items // internal convention: We add "NEW" to the media ID to show that we have // the new iOPAC version Element input = cell.select("input[name=MedNrVerlAll]").first(); String value = input.val(); item.setProlongData("NEW" + value); item.setId(value.split(";")[0]); if (input.hasAttr("disabled")) item.setRenewable(false); } else { // previous versions - link for prolonging on every medium String link = cell.select("a").attr("href"); item.setProlongData(link); // find media number with regex Pattern pattern = Pattern.compile("mednr=([^&]*)&"); Matcher matcher = pattern.matcher(link); if (matcher.find() && matcher.group() != null) item.setId(matcher.group(1)); } } } media.add(item); } assert (media.size() == trs - 1); } static void parseResList(List<ReservedItem> media, Document doc, JSONObject data) { if (doc.select("a[name=RES]").size() == 0) return; Elements copytrs = doc.select("a[name=RES] ~ table:contains(Titel)").first().select("tr"); doc.setBaseUri(data.optString("baseurl")); DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); int trs = copytrs.size(); if (trs < 2) { return; } assert (trs > 0); for (int i = 1; i < trs; i++) { Element tr = copytrs.get(i); ReservedItem item = new ReservedItem(); item.setTitle(tr.child(0).text().trim().replace("\u00a0", "")); item.setAuthor(tr.child(1).text().trim().replace("\u00a0", "")); String readyDate = tr.child(4).text().trim().replace("\u00a0", ""); if (readyDate.equals("")) { item.setStatus("bereit"); } else { try { item.setReadyDate( fmt.parseLocalDate(readyDate)); } catch (IllegalArgumentException e) { item.setStatus(readyDate); } } if (tr.select("a").size() > 0) { item.setCancelData(tr.select("a").last().attr("href")); } media.add(item); } assert (media.size() == trs - 1); } private SearchField createSearchField(Element descTd, Element inputTd) { String name = descTd.select("span, blockquote").text().replace(":", "") .trim().replace("\u00a0", ""); if (inputTd.select("select").size() > 0 && !name.equals("Treffer/Seite") && !name.equals("Medientypen") && !name.equals("Medientyp") && !name.equals("Treffer pro Seite")) { Element select = inputTd.select("select").first(); DropdownSearchField field = new DropdownSearchField(); field.setDisplayName(name); field.setId(select.attr("name")); for (Element option : select.select("option")) { field.addDropdownValue(option.attr("value"), option.text()); } return field; } else if (inputTd.select("input").size() > 0) { TextSearchField field = new TextSearchField(); Element input = inputTd.select("input").first(); field.setDisplayName(name); field.setId(input.attr("name")); field.setHint(""); return field; } else { return null; } } @Override public List<SearchField> parseSearchFields() throws IOException { List<SearchField> fields = new ArrayList<>(); // Extract all search fields, except media types String html; try { html = httpGet(opac_url + dir + "/search_expert.htm", getDefaultEncoding()); } catch (NotReachableException e) { html = httpGet(opac_url + dir + "/iopacie.htm", getDefaultEncoding()); } Document doc = Jsoup.parse(html); Elements trs = doc .select("form tr:has(input:not([type=submit], [type=reset])), form tr:has(select)"); for (Element tr : trs) { Elements tds = tr.children(); if (tds.size() == 4) { // Two search fields next to each other in one row SearchField field1 = createSearchField(tds.get(0), tds.get(1)); SearchField field2 = createSearchField(tds.get(2), tds.get(3)); if (field1 != null) { fields.add(field1); } if (field2 != null) { fields.add(field2); } } else if (tds.size() == 2 || (tds.size() == 3 && tds.get(2).children().size() == 0)) { SearchField field = createSearchField(tds.get(0), tds.get(1)); if (field != null) { fields.add(field); } } } if (fields.size() == 0 && doc.select("[name=sleStichwort]").size() > 0) { TextSearchField field = new TextSearchField(); Element input = doc.select("input[name=sleStichwort]").first(); field.setDisplayName(stringProvider .getString(StringProvider.FREE_SEARCH)); field.setId(input.attr("name")); field.setHint(""); fields.add(field); } // Extract available media types. // We have to parse JavaScript. Doing this with RegEx is evil. // But not as evil as including a JavaScript VM into the app. // And I honestly do not see another way. Pattern pattern_key = Pattern .compile("mtyp\\[[0-9]+\\]\\[\"typ\"\\] = \"([^\"]+)\";"); Pattern pattern_value = Pattern .compile("mtyp\\[[0-9]+\\]\\[\"bez\"\\] = \"([^\"]+)\";"); DropdownSearchField mtyp = new DropdownSearchField(); try { try { html = httpGet(opac_url + dir + "/mtyp.js", getDefaultEncoding()); } catch (NotReachableException e) { html = httpGet(opac_url + "/mtyp.js", getDefaultEncoding()); } String[] parts = html.split("new Array\\(\\);"); for (String part : parts) { Matcher matcher1 = pattern_key.matcher(part); String key = ""; String value = ""; if (matcher1.find()) { key = matcher1.group(1); } Matcher matcher2 = pattern_value.matcher(part); if (matcher2.find()) { value = matcher2.group(1); } if (!value.equals("")) { mtyp.addDropdownValue(key, value); } } } catch (IOException e) { try { html = httpGet(opac_url + dir + "/frames/search_form.php?bReset=1?bReset=1", getDefaultEncoding()); doc = Jsoup.parse(html); for (Element opt : doc.select("#imtyp option")) { mtyp.addDropdownValue(opt.attr("value"), opt.text()); } } catch (IOException e1) { e1.printStackTrace(); } } if (mtyp.getDropdownValues() != null && !mtyp.getDropdownValues().isEmpty()) { mtyp.setDisplayName("Medientypen"); mtyp.setId("Medientyp"); fields.add(mtyp); } return fields; } @Override public String getShareUrl(String id, String title) { if (newShareLinks) { return opac_url + dir + "/?mednr=" + id; } else { return opac_url + "/cgi-bin/di.exe?cMedNr=" + id + "&mode=23"; } } @Override public int getSupportFlags() { return SUPPORT_FLAG_ENDLESS_SCROLLING | SUPPORT_FLAG_CHANGE_ACCOUNT | SUPPORT_FLAG_ACCOUNT_PROLONG_ALL; } public void updateRechnr(Document doc) { String url = null; for (Element a : doc.select("table a")) { if (a.attr("href").contains("rechnr=")) { url = a.attr("href"); break; } } if (url == null) { return; } Integer rechnrPosition = url.indexOf("rechnr=") + 7; rechnr = url .substring(rechnrPosition, url.indexOf("&", rechnrPosition)); } @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; } }