/* * Copyright (C) 2015 by Rüdiger Wurth, Raphael Michel and contributors 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.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.message.BasicNameValuePair; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.json.JSONArray; 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.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.HttpUtils; 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.reporting.Report; import de.geeksfactory.opacclient.reporting.ReportHandler; import de.geeksfactory.opacclient.searchfields.BarcodeSearchField; import de.geeksfactory.opacclient.searchfields.DropdownSearchField; import de.geeksfactory.opacclient.searchfields.SearchField; import de.geeksfactory.opacclient.searchfields.SearchQuery; import de.geeksfactory.opacclient.searchfields.TextSearchField; /** * @author Ruediger Wurth */ public class BiBer1992 extends BaseApi { protected static HashMap<String, MediaType> defaulttypes = new HashMap<>(); static { } // we have to limit num of results because PUSH attribute SHOW=20 does not work: // number of results is always 50 which is too much final private int numOfResultsPerPage = 20; protected boolean newStyleReservations = false; private String opacUrl = ""; private String opacDir = "opac"; // sometimes also "opax" private String opacSuffix = ".C"; // sometimes also ".S" private JSONObject data; // private int m_resultcount = 10; // private long logged_in; // private Account logged_in_as; private List<NameValuePair> nameValuePairs = new ArrayList<>(2); /* * ----- media types ----- Example Wuerzburg: <td ...><input type="checkbox" * name="MT" value="1" ...></td> <td ...><img src="../image/spacer.gif.S" * title="Buch"><br>Buch</td> * * Example Friedrichshafen: <td ...><input type="checkbox" name="MS" * value="1" ...></td> <td ...><img src="../image/spacer.gif.S" * title="Buch"><br>Buch</td> * * Example Offenburg: <input type="radio" name="MT" checked * value="MTYP0">Alles   <input type="radio" name="MT" * value="MTYP10">Belletristik   Unfortunately Biber miss the end * tag </input>, so opt.text() does not work! (at least Offenburg) * * Example Essen, Aschaffenburg: <input type="radio" name="MT" checked * value="MTYP0"><img src="../image/all.gif.S" title="Alles"> <input * type="radio" name="MT" value="MTYP7"><img src="../image/cdrom.gif.S" * title="CD-ROM"> * * ----- Branches ----- Example Essen,Erkrath: no closing </option> !!! * cannot be parsed by Jsoup, so not supported <select name="AORT"> <option * value="ZWST1">Altendorf </select> * * Example Hagen, Würzburg, Friedrichshafen: <select name="ZW" class="sel1"> * <option selected value="ZWST0">Alle Bibliotheksorte</option> </select> */ @Override public List<SearchField> parseSearchFields() throws IOException { List<SearchField> fields = new ArrayList<>(); HttpGet httpget; if (opacDir.contains("opax")) { httpget = new HttpGet(opacUrl + "/" + opacDir + "/de/qsel.html.S"); } else { httpget = new HttpGet(opacUrl + "/" + opacDir + "/de/qsel_main.S"); } HttpResponse response = http_client.execute(httpget); if (response.getStatusLine().getStatusCode() == 500) { throw new NotReachableException(response.getStatusLine().getReasonPhrase()); } String html = convertStreamToString(response.getEntity().getContent()); HttpUtils.consume(response.getEntity()); Document doc = Jsoup.parse(html); // get text fields Elements text_opts = doc.select("form select[name=REG1] option"); for (Element opt : text_opts) { TextSearchField field = new TextSearchField(); field.setId(opt.attr("value")); field.setDisplayName(opt.text()); field.setHint(""); fields.add(field); } // get media types Elements mt_opts = doc.select("form input[name~=(MT|MS)]"); if (mt_opts.size() > 0) { DropdownSearchField mtDropdown = new DropdownSearchField(); mtDropdown.setId(mt_opts.get(0).attr("name")); mtDropdown.setDisplayName("Medientyp"); for (Element opt : mt_opts) { if (!opt.val().equals("")) { String text = opt.text(); if (text.length() == 0) { // text is empty, check layouts: // Essen: <input name="MT"><img title="mediatype"> // Schaffenb: <input name="MT"><img alt="mediatype"> Element img = opt.nextElementSibling(); if (img != null && img.tagName().equals("img")) { text = img.attr("title"); if (text.equals("")) { text = img.attr("alt"); } } } if (text.length() == 0) { // text is still empty, check table layout, Example // Friedrichshafen // <td><input name="MT"></td> <td><img // title="mediatype"></td> Element td1 = opt.parent(); Element td2 = td1.nextElementSibling(); if (td2 != null) { Elements td2Children = td2.select("img[title]"); if (td2Children.size() > 0) { text = td2Children.get(0).attr("title"); } } } if (text.length() == 0) { // text is still empty, check images in label layout, Example // Wiedenst // <input type="radio" name="MT" id="MTYP1" value="MTYP1"> // <label for="MTYP1"><img src="http://www.wiedenest.de/bib/image/books // .png" alt="Bücher" title="Bücher"></label> Element label = opt.nextElementSibling(); if (label != null) { Elements td2Children = label.select("img[title]"); if (td2Children.size() > 0) { text = td2Children.get(0).attr("title"); } } } if (text.length() == 0) { // text is still empty: missing end tag like Offenburg text = parse_option_regex(opt); } mtDropdown.addDropdownValue(opt.val(), text); } } fields.add(mtDropdown); } // get branches Elements br_opts = doc.select("form select[name=ZW] option"); if (br_opts.size() > 0) { DropdownSearchField brDropdown = new DropdownSearchField(); brDropdown.setId(br_opts.get(0).parent().attr("name")); brDropdown.setDisplayName(br_opts.get(0).parent().parent() .previousElementSibling().text().replace("\u00a0", "") .replace("?", "").trim()); for (Element opt : br_opts) { brDropdown.addDropdownValue(opt.val(), opt.text()); } fields.add(brDropdown); } Elements sort_opts = doc.select("form select[name=SORTX] option"); if (sort_opts.size() > 0) { DropdownSearchField sortDropdown = new DropdownSearchField(); sortDropdown.setId(sort_opts.get(0).parent().attr("name")); sortDropdown.setDisplayName(sort_opts.get(0).parent().parent() .previousElementSibling().text() .replace("\u00a0", "") .replace("?", "").trim()); for (Element opt : sort_opts) { sortDropdown.addDropdownValue(opt.val(), opt.text()); } fields.add(sortDropdown); } return fields; } private static MediaType getMediaTypeFromImageFilename(SearchResult sr, String imagename, JSONObject data) { String[] fparts1 = imagename.split("/"); // "images/31.gif.S" String[] fparts2 = fparts1[fparts1.length - 1].split("\\."); // "31.gif.S" String lookup = fparts2[0]; // "31" if (imagename.contains("amazon")) { if (sr != null) sr.setCover(imagename); return null; } if (data.has("mediatypes")) { try { String typeStr = data.getJSONObject("mediatypes").getString( lookup); return MediaType.valueOf(typeStr); } catch (Exception e) { if (defaulttypes.containsKey(lookup)) { return defaulttypes.get(lookup); } } } else { if (defaulttypes.containsKey(lookup)) { return defaulttypes.get(lookup); } } return null; } /* * Parser for non XML compliant html part: (the crazy way) Get text from * <input> without end tag </input> * * Example Offenburg: <input type="radio" name="MT" * value="MTYP10">Belletristik   Regex1: value="MTYP10".*?>([^<]+) */ private String parse_option_regex(Element inputTag) { String optStr = inputTag.val(); String html = inputTag.parent().html(); String result = optStr; String regex1 = "value=\"" + optStr + "\".*?>([^<]+)"; String[] regexList = new String[]{regex1}; for (String regex : regexList) { Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(html); if (matcher.find()) { result = matcher.group(1); result = result.replaceAll(" ", " ").trim(); break; } } return result; } @Override public void init(Library lib, HttpClientFactory httpClientFactory) { super.init(lib, httpClientFactory); data = lib.getData(); try { opacUrl = data.getString("baseurl"); opacDir = data.getString("opacdir"); opacSuffix = data.optString("opacsuffix", ".C"); } catch (JSONException e) { throw new RuntimeException(e); } } @Override public SearchRequestResult search(List<SearchQuery> queryList) throws IOException { if (!initialised) { start(); } nameValuePairs.clear(); int count = 1; for (SearchQuery query : queryList) { if ((query.getSearchField() instanceof TextSearchField || query .getSearchField() instanceof BarcodeSearchField) && !query.getValue().equals("")) { nameValuePairs.add(new BasicNameValuePair("CNN" + count, "AND")); nameValuePairs.add(new BasicNameValuePair("FLD" + count, query.getValue())); nameValuePairs.add(new BasicNameValuePair("REG" + count, query.getKey())); count++; } else if (query.getSearchField() instanceof DropdownSearchField) { nameValuePairs.add(new BasicNameValuePair(query.getKey(), query.getValue())); } } nameValuePairs.add(new BasicNameValuePair("FUNC", "qsel")); nameValuePairs.add(new BasicNameValuePair("LANG", "de")); nameValuePairs.add(new BasicNameValuePair("SHOW", "20")); // but // result // gives 50 nameValuePairs.add(new BasicNameValuePair("SHOWSTAT", "N")); nameValuePairs.add(new BasicNameValuePair("FROMPOS", "1")); return searchGetPage(1); } /* * (non-Javadoc) * * @see OpacApi#searchGetPage(int) */ @Override public SearchRequestResult searchGetPage(int page) throws IOException { int startNum = (page - 1) * numOfResultsPerPage + 1; // remove last element = "FROMPOS", and add a new one nameValuePairs.remove(nameValuePairs.size() - 1); nameValuePairs.add(new BasicNameValuePair("FROMPOS", String .valueOf(startNum))); String html = httpPost(opacUrl + "/" + opacDir + "/query" + opacSuffix, new UrlEncodedFormEntity(nameValuePairs), getDefaultEncoding()); return parse_search(html, page); } /* * result table format: JSON "rows_per_hit" = 1: One <tr> per hit JSON * "rows_per_hit" = 2: Two <tr> per hit (default) <form> <table> <tr * valign="top"> <td class="td3" ...><a href=...><img ...></a></td> (row is * optional, only in some bibs) <td class="td2" ...><input ...></td> <td * width="34%">TITEL</td> <td width="34%"> </td> <td width="6%" * align="center">2009</td> <td width="*" align="left">DVD0 Seew</td> </tr> * <tr valign="top"> <td class="td3" ...> ...</td> <td class="td2" * ...> ...</td> <td colspan="4" ...><font size="-1"><font * class="p1">Erwachsenenbibliothek</font></font><div * class="hr4"></div></td> </tr> */ private SearchRequestResult parse_search(String html, int page) { List<SearchResult> results = new ArrayList<>(); Document doc = Jsoup.parse(html); if (doc.select("h3").text().contains("Es wurde nichts gefunden")) { return new SearchRequestResult(results, 0, page); } Elements trList = doc.select("form table tr[valign]"); // <tr // valign="top"> Elements elem; int rows_per_hit = 2; if (trList.size() == 1 || (trList.size() > 1 && trList.get(0).select("input[type=checkbox]").size() > 0 && trList .get(1).select("input[type=checkbox]").size() > 0)) { rows_per_hit = 1; } try { rows_per_hit = data.getInt("rows_per_hit"); } catch (JSONException e) { } // Overall search results // are very differently layouted, but have always the text: // "....Treffer Gesamt (nnn)" int results_total; Pattern pattern = Pattern.compile("Treffer Gesamt \\(([0-9]+)\\)"); Matcher matcher = pattern.matcher(html); if (matcher.find()) { results_total = Integer.parseInt(matcher.group(1)); } else { results_total = -1; } // limit to 20 entries int numOfEntries = trList.size() / rows_per_hit; // two rows per entry if (numOfEntries > numOfResultsPerPage) { numOfEntries = numOfResultsPerPage; } for (int i = 0; i < numOfEntries; i++) { Element tr = trList.get(i * rows_per_hit); SearchResult sr = new SearchResult(); // ID as href tag elem = tr.select("td a"); if (elem.size() > 0) { String hrefID = elem.get(0).attr("href"); sr.setId(hrefID); } else { // no ID as href found, look for the ID in the input form elem = tr.select("td input"); if (elem.size() > 0) { String nameID = elem.get(0).attr("name").trim(); String hrefID = "/" + opacDir + "/ftitle" + opacSuffix + "?LANG=de&FUNC=full&" + nameID + "=YES"; sr.setId(hrefID); } } // media type elem = tr.select("td img"); if (elem.size() > 0) { sr.setType(getMediaTypeFromImageFilename(sr, elem.get(0).attr("src"), data)); } // description String desc = ""; try { // array "searchtable" list the column numbers of the // description JSONArray searchtable = data.getJSONArray("searchtable"); for (int j = 0; j < searchtable.length(); j++) { int colNum = searchtable.getInt(j); if (j > 0) { desc = desc + "<br />"; } String c = tr.child(colNum).html(); if (tr.child(colNum).childNodes().size() == 1 && tr.child(colNum).select("a[href*=ftitle.]").size() > 0) { c = tr.select("a[href*=ftitle.]").text(); } desc = desc + c; } } catch (Exception e) { e.printStackTrace(); } // remove links "<a ...>...</a> // needed for Friedrichshafen: "Warenkorb", "Vormerkung" // Herford: "Medienkorb" desc = desc.replaceAll("<a .*?</a>", ""); // remove newlines (useless in HTML) desc = desc.replaceAll("\\n", ""); // remove hidden divs ("Titel übernommen!" in Wuerzburg) desc = desc.replaceAll("<div[^>]*style=\"display:none\">.*</div>", ""); // remove all invalid HTML tags desc = desc.replaceAll("</?(tr|td|font|table|tbody|div)[^>]*>", ""); // replace multiple line breaks by one desc = desc.replaceAll("(<br( /)?>\\s*)+", "<br>"); sr.setInnerhtml(desc); if (tr.select("font.p04x09b").size() > 0 && tr.select("font.p02x09b").size() == 0) { sr.setStatus(Status.GREEN); } else if (tr.select("font.p04x09b").size() == 0 && tr.select("font.p02x09b").size() > 0) { sr.setStatus(Status.RED); } else if (tr.select("font.p04x09b").size() > 0 && tr.select("font.p02x09b").size() > 0) { sr.setStatus(Status.YELLOW); } // number sr.setNr(i / rows_per_hit); results.add(sr); } // m_resultcount = results.size(); return new SearchRequestResult(results, results_total, page); } /* * (non-Javadoc) * * @see * OpacApi#getResultById(java.lang.String) */ @Override public DetailedItem getResultById(String id, String homebranch) throws IOException { if (!initialised) { start(); } if (!id.contains("ftitle")) { id = "ftitle" + opacSuffix + "?LANG=de&FUNC=full&" + id + "=YES"; } // normally full path like // "/opac/ftitle.C?LANG=de&FUNC=full&331313252=YES" // but sometimes (Wuerzburg) "ftitle.C?LANG=de&FUNC=full&331313252=YES" // and sometimes (Hagen) absolute URL including opac_url if (id.startsWith(opacUrl)) { id = id.substring(opacUrl.length()); } else if (!id.startsWith("/")) { id = "/" + opacDir + "/" + id; } HttpGet httpget = new HttpGet(opacUrl + id); HttpResponse response = http_client.execute(httpget); String html = convertStreamToString(response.getEntity().getContent()); HttpUtils.consume(response.getEntity()); return parse_result(html); } /* * (non-Javadoc) * * @see OpacApi#getResult(int) */ @Override public DetailedItem getResult(int position) throws IOException { // not needed, normall all search results should have an ID, // so getResultById() is called return null; } /* * Two-column table inside of a form 1st column is category, e.g. * "Verfasser" 2nd column is content, e.g. "Bach, Johann Sebastian" In some * rows, the 1st column is empty, then 2nd column is continued text from row * above. * * Some libraries have a second section for the copies in stock (Exemplare). * This 2nd section has reverse layout. * * |-------------------| | Subject | Content | |-------------------| | * Subject | Content | |-------------------| | | Content | * |-------------------| | Subject | Content | * |-------------------------------------------------| | | Site | Signatur| * ID | State | |-------------------------------------------------| | | * Content | Content | Content | Content | * |-------------------------------------------------| */ private DetailedItem parse_result(String html) { DetailedItem item = new DetailedItem(); Document document = Jsoup.parse(html); Elements rows = document.select("html body form table tr"); // Elements rows = document.select("html body div form table tr"); // Element rowReverseSubject = null; Detail detail = null; // prepare copiestable Copy copy_last_content = null; int copy_row = 0; String[] copy_keys = new String[]{"barcode", "branch", "department", "location", "status", "returndate", "reservations" }; int[] copy_map = new int[]{3, 1, -1, 1, 4, -1, -1}; try { JSONObject map = data.getJSONObject("copiestable"); for (int i = 0; i < copy_keys.length; i++) { if (map.has(copy_keys[i])) { copy_map[i] = map.getInt(copy_keys[i]); } } } catch (Exception e) { // "copiestable" is optional } DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); // go through all rows for (Element row : rows) { Elements columns = row.children(); if (columns.size() == 2) { // HTML tag " " is encoded as 0xA0 String firstColumn = columns.get(0).text() .replace("\u00a0", " ").trim(); String secondColumn = columns.get(1).text() .replace("\u00a0", " ").trim(); if (firstColumn.length() > 0) { // 1st column is category if (firstColumn.equalsIgnoreCase("titel")) { detail = null; item.setTitle(secondColumn); } else { if (secondColumn.contains("hier klicken") && columns.get(1).select("a").size() > 0) { secondColumn += " " + columns.get(1).select("a").first() .attr("href"); } detail = new Detail(firstColumn, secondColumn); item.getDetails().add(detail); } } else { // 1st column is empty, so it is an extension to last // category if (detail != null) { String content = detail.getContent() + "\n" + secondColumn; detail.setContent(content); } else { // detail==0, so it's the first row // check if there is an amazon image if (columns.get(0).select("a img[src]").size() > 0) { item.setCover(columns.get(0).select("a img") .first().attr("src")); } } } } else if (columns.size() > 3) { // This is the second section: the copies in stock ("Exemplare") // With reverse layout: first row is headline, skipped via // (copy_row > 0) if (copy_row > 0) { Copy copy = new Copy(); for (int j = 0; j < copy_keys.length; j++) { int col = copy_map[j]; if (col > -1) { String text = ""; if (copy_keys[j].equals("branch")) { // for "Standort" only use ownText() to suppress // Link "Wegweiser" text = columns.get(col).ownText() .replace("\u00a0", " ").trim(); } if (text.length() == 0) { // text of children text = columns.get(col).text() .replace("\u00a0", " ").trim(); } if (text.length() == 0) { // empty table cell, take the one above // this is sometimes the case for "Standort" if (copy_keys[j].equals("status")) { // but do it not for Status text = " "; } else { if (copy_last_content != null) { text = copy_last_content .get(copy_keys[j]); } else { text = ""; } } } if (copy_keys[j].equals("reservations")) { text = text.replace("Vorgemerkt: ", "") .replace("Vorbestellt: ", ""); } try { copy.set(copy_keys[j], text, fmt); } catch (IllegalArgumentException e) { e.printStackTrace(); } } } if (copy.getBranch() != null && copy.getLocation() != null && copy.getLocation().equals(copy.getBranch())) { copy.setLocation(null); } item.addCopy(copy); copy_last_content = copy; }// ignore 1st row copy_row++; }// if columns.size }// for rows item.setReservable(true); // We cannot check if media is reservable if (opacDir.contains("opax")) { if (document.select("input[type=checkbox]").size() > 0) { item.setReservation_info(document .select("input[type=checkbox]").first().attr("name")); } else if (document.select("a[href^=reserv" + opacSuffix + "]").size() > 0) { String href = document.select("a[href^=reserv" + opacSuffix + "]").first() .attr("href"); item.setReservation_info(href.substring(href.indexOf("resF_"))); } else { item.setReservable(false); } } else { item.setReservation_info(document.select("input[name=ID]").attr( "value")); } return item; } /* * (non-Javadoc) * * @see * OpacApi#reservation(java.lang.String, * de.geeksfactory.opacclient.objects.Account, int, java.lang.String) */ @Override public ReservationResult reservation(DetailedItem item, Account account, int useraction, String selection) throws IOException { String resinfo = item.getReservation_info(); if (selection == null || selection.equals("confirmed")) { // STEP 1: Check if reservable and select branch ("ID1") // Differences between opax and opac String func = opacDir.contains("opax") ? "sigl" : "resF"; String id = opacDir.contains("opax") ? (resinfo.contains("resF") ? resinfo .substring(5) + "=" + resinfo : resinfo + "=resF_" + resinfo) : "ID=" + resinfo; String html = httpGet(opacUrl + "/" + opacDir + "/reserv" + opacSuffix + "?LANG=de&FUNC=" + func + "&" + id, getDefaultEncoding()); Document doc = Jsoup.parse(html); newStyleReservations = doc .select("input[name=" + resinfo.replace("resF_", "") + "]") .val().length() > 4; Elements optionsElements = doc.select("select[name=ID1] option"); if (optionsElements.size() > 0) { List<Map<String, String>> options = new ArrayList<>(); for (Element option : optionsElements) { if ("0".equals(option.attr("value"))) { continue; } Map<String, String> selopt = new HashMap<>(); selopt.put("key", option.attr("value") + ":" + option.text()); selopt.put("value", option.text()); options.add(selopt); } if (options.size() > 1) { ReservationResult res = new ReservationResult( MultiStepResult.Status.SELECTION_NEEDED); res.setActionIdentifier(ReservationResult.ACTION_BRANCH); res.setSelection(options); return res; } else { return reservation(item, account, useraction, options.get(0).get("key")); } } else { ReservationResult res = new ReservationResult( MultiStepResult.Status.ERROR); res.setMessage("Dieses Medium ist nicht reservierbar."); return res; } } else { // STEP 2: Reserve List<NameValuePair> nameValuePairs = new ArrayList<>(); nameValuePairs.add(new BasicNameValuePair("LANG", "de")); nameValuePairs.add(new BasicNameValuePair("BENUTZER", account .getName())); nameValuePairs.add(new BasicNameValuePair("PASSWORD", account .getPassword())); nameValuePairs.add(new BasicNameValuePair("FUNC", "vors")); if (opacDir.contains("opax")) { nameValuePairs.add(new BasicNameValuePair(resinfo.replace( "resF_", ""), "vors" + (newStyleReservations ? resinfo.replace("resF_", "") : ""))); } if (newStyleReservations) { nameValuePairs.add(new BasicNameValuePair("ID11", selection .split(":")[1])); } nameValuePairs.add(new BasicNameValuePair("ID1", selection .split(":")[0])); String html = httpPost(opacUrl + "/" + opacDir + "/setreserv" + opacSuffix, new UrlEncodedFormEntity(nameValuePairs), getDefaultEncoding()); Document doc = Jsoup.parse(html); if (doc.select(".tab21 .p44b, .p2").text().contains("eingetragen")) { return new ReservationResult(MultiStepResult.Status.OK); } else { ReservationResult res = new ReservationResult( MultiStepResult.Status.ERROR); if (doc.select(".p1, .p22b").size() > 0) { res.setMessage(doc.select(".p1, .p22b").text()); } return res; } } } /* * (non-Javadoc) * * @see * OpacApi#prolong(de.geeksfactory.opacclient * .objects.Account, java.lang.String) * * Offenburg, prolong negative result: <table border="1" width="100%"> <tr> * <th ...>Nr</th> <th ...>Signatur / Kurztitel</th> <th * ...>Fällig</th> <th ...>Status</th> </tr> <tr> <td * ...>101103778</td> <td ...>Hyde / Hyde, Anthony: Der Mann aus </td> <td * ...>09.04.2013</td> <td ...><font class="p1">verlängerbar ab * 03.04.13, nicht verlängert</font> <br>Bitte wenden Sie sich an Ihre * Bibliothek!</td> </tr> </table> * * Offenburg, prolong positive result: TO BE DESCRIBED */ @Override public ProlongResult prolong(String media, Account account, int useraction, String Selection) throws IOException { String command; // prolong media via http POST // Offenburg: URL is .../opac/verl.C // Hagen: URL is .../opax/renewmedia.C if (opacDir.contains("opax")) { command = "/renewmedia" + opacSuffix; } else { command = "/verl" + opacSuffix; } List<NameValuePair> nameValuePairs = new ArrayList<>(2); nameValuePairs.add(new BasicNameValuePair(media, "YES")); nameValuePairs .add(new BasicNameValuePair("BENUTZER", account.getName())); nameValuePairs.add(new BasicNameValuePair("FUNC", "verl")); nameValuePairs.add(new BasicNameValuePair("LANG", "de")); nameValuePairs.add(new BasicNameValuePair("PASSWORD", account .getPassword())); String html = httpPost(opacUrl + "/" + opacDir + command, new UrlEncodedFormEntity(nameValuePairs), getDefaultEncoding()); if (html.contains("no such key")) { html = httpPost( opacUrl + "/" + opacDir + command.replace(".C", ".S"), new UrlEncodedFormEntity(nameValuePairs), getDefaultEncoding()); } Document doc = Jsoup.parse(html); // Check result: // First we look for a cell with text "Status" // and store the column number // Then we look in the rows below at this column if // we find any text. Stop at first text we find. // This text must start with "verl�ngert" Elements rowElements = doc.select("table tr"); int statusCol = -1; // Status column not yet found // rows loop for (int i = 0; i < rowElements.size(); i++) { Element tr = rowElements.get(i); Elements tdList = tr.children(); // <th> or <td> // columns loop for (int j = 0; j < tdList.size(); j++) { String cellText = tdList.get(j).text().trim(); if (statusCol < 0) { // we look for cell with text "Status" if (cellText.equals("Status")) { statusCol = j; break; // next row } } else { // we look only at Status column // In "Hagen", there are some extra empty rows below if ((j == statusCol) && (cellText.length() > 0)) { // Status found if (cellText.matches("verl.ngert.*")) { return new ProlongResult(MultiStepResult.Status.OK); } else { return new ProlongResult( MultiStepResult.Status.ERROR, cellText); } } } }// for columns }// for rows return new ProlongResult(MultiStepResult.Status.ERROR, "unknown result"); } /* * (non-Javadoc) * * @see * OpacApi#cancel(de.geeksfactory.opacclient * .objects.Account, java.lang.String) */ @Override public CancelResult cancel(String media, Account account, int useraction, String selection) throws IOException, OpacErrorException { List<NameValuePair> nameValuePairs = new ArrayList<>(); nameValuePairs.add(new BasicNameValuePair("LANG", "de")); nameValuePairs.add(new BasicNameValuePair("FUNC", "vorl")); if (opacDir.contains("opax")) { nameValuePairs.add(new BasicNameValuePair("BENUTZER", account .getName())); nameValuePairs.add(new BasicNameValuePair("PASSWORD", account .getPassword())); } nameValuePairs.add(new BasicNameValuePair(media, "YES")); String action = opacDir.contains("opax") ? "/delreserv" + opacSuffix : "/vorml" + opacSuffix; String html = httpPost(opacUrl + "/" + opacDir + action, new UrlEncodedFormEntity(nameValuePairs), getDefaultEncoding()); Document doc = Jsoup.parse(html); if (doc.select(".tab21 .p44b, .p2").text().contains("Vormerkung wurde")) { return new CancelResult(MultiStepResult.Status.OK); } else { return new CancelResult(MultiStepResult.Status.ERROR); } } /* * (non-Javadoc) * * @see * OpacApi#account(de.geeksfactory.opacclient * .objects.Account) * * POST-format: BENUTZER xxxxxxxxx FUNC medk LANG de PASSWORD ddmmyyyy */ @Override public AccountData account(Account account) throws IOException, JSONException, OpacErrorException { AccountData res = new AccountData(account.getId()); // get media List<LentItem> media = accountGetMedia(account, res); res.setLent(media); // get reservations List<ReservedItem> reservations = accountGetReservations(account); res.setReservations(reservations); return res; } private List<LentItem> accountGetMedia(Account account, AccountData res) throws IOException, JSONException, OpacErrorException { // get media list via http POST Document doc = accountHttpPost(account, "medk"); return parseMediaList(res, account, doc, data, reportHandler, loadJsonResource("/biber1992/headers_lent.json")); } static List<LentItem> parseMediaList(AccountData res, Account account, Document doc, JSONObject data, ReportHandler reportHandler, JSONObject headers_lent) throws JSONException { List<LentItem> media = new ArrayList<>(); if (doc == null) { return media; } if (doc.select("form[name=medkl] table").size() == 0){ return new ArrayList<LentItem>(); } // parse result list Map<String, Integer> copymap = new HashMap<>(); Elements headerCells = doc.select("form[name=medkl] table tr:has(th)").last().select("th"); JSONArray headersList = new JSONArray(); JSONArray unknownHeaders = new JSONArray(); int j = 0; for (Element headerCell : headerCells) { String header = headerCell.text(); headersList.put(header); if (headers_lent.has(header)) { if (!headers_lent.isNull(header)) copymap.put(headers_lent.getString(header), j); } else { unknownHeaders.put(header); } j++; } if (unknownHeaders.length() > 0) { // send report JSONObject reportData = new JSONObject(); reportData.put("headers", headersList); reportData.put("unknown_headers", unknownHeaders); Report report = new Report(account.getLibrary(), "biber1992", "unknown header - lent", DateTime.now(), reportData); reportHandler.sendReport(report); // fallback to JSON JSONObject accounttable = data.getJSONObject("accounttable"); copymap = jsonToMap(accounttable); } Pattern expire = Pattern.compile("Ausweisg.ltigkeit: ([0-9.]+)"); Pattern fees = Pattern.compile("([0-9,.]+) ."); for (Element td : doc.select(".td01x09n")) { String text = td.text().trim(); if (expire.matcher(text).matches()) { res.setValidUntil(expire.matcher(text).replaceAll("$1")); } else if (fees.matcher(text).matches()) { res.setPendingFees(text); } } DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); Elements rowElements = doc.select("form[name=medkl] table tr"); // rows: skip 1st row -> title row for (int i = 1; i < rowElements.size(); i++) { Element tr = rowElements.get(i); if (tr.child(0).tagName().equals("th")) { continue; } LentItem item = new LentItem(); Elements mediatypeImg = tr.select("td img"); if (mediatypeImg.size() > 0) { item.setMediaType(getMediaTypeFromImageFilename( null, mediatypeImg.get(0).attr("src"), data)); } Pattern itemIdPat = Pattern .compile("javascript:(?:smAcc|smMedk)\\('[a-z]+','[a-z]+','([A-Za-z0-9]+)'\\)"); // columns: all elements of one media for (Map.Entry<String, Integer> entry : copymap.entrySet()) { String key = entry.getKey(); int index = entry.getValue(); if (tr.child(index).select("a").size() == 1) { Matcher matcher = itemIdPat.matcher(tr.child(index) .select("a").attr("href")); if (matcher.find()) item.setId(matcher.group(1)); } String value = tr.child(index).text().trim().replace("\u00A0", ""); switch (key) { case "author+title": item.setTitle(findTitleAndAuthor(value)[0]); item.setAuthor(findTitleAndAuthor(value)[1]); continue; case "returndate": try { value = fmt.parseLocalDate(value).toString(); } catch (IllegalArgumentException e1) { e1.printStackTrace(); } break; case "renewals_number": case "status": if (value != null && value.length() != 0) { if (item.getStatus() == null) { item.setStatus(value); } else { item.setStatus(item.getStatus() + ", " + value); } } continue; } if (value != null && value.length() != 0) item.set(key, value); } if (tr.select("input[type=checkbox][value=YES]").size() > 0) { item.setProlongData(tr.select("input[type=checkbox][value=YES]").attr("name")); } media.add(item); } return media; } private List<ReservedItem> accountGetReservations(Account account) throws IOException, JSONException, OpacErrorException { // get reservations list via http POST Document doc = accountHttpPost(account, "vorm"); return parseResList(account, doc, data, reportHandler, loadJsonResource("/biber1992/headers_reservations.json")); } static List<ReservedItem> parseResList(Account account, Document doc, JSONObject data, ReportHandler reportHandler, JSONObject headers_reservations) throws JSONException { List<ReservedItem> reservations = new ArrayList<>(); if (doc == null) { // error message as html result return reservations; } if (doc.select("form[name=vorml] table").size() == 0){ return new ArrayList<ReservedItem>(); } // parse result list Map<String, Integer> copymap = new HashMap<>(); Elements headerCells = doc.select("form[name=vorml] table tr:has(th)").last().select("th"); JSONArray headersList = new JSONArray(); JSONArray unknownHeaders = new JSONArray(); int j = 0; for (Element headerCell : headerCells) { String header = headerCell.text(); headersList.put(header); if (headers_reservations.has(header)) { if (!headers_reservations.isNull(header)) { copymap.put(headers_reservations.getString(header), j); } } else { unknownHeaders.put(header); } j++; } if (unknownHeaders.length() > 0) { // send report JSONObject reportData = new JSONObject(); reportData.put("headers", headersList); reportData.put("unknown_headers", unknownHeaders); Report report = new Report(account.getLibrary(), "biber1992", "unknown header - reservations", DateTime.now(), reportData); reportHandler.sendReport(report); // fallback to JSON JSONObject reservationtable; if (data.has("reservationtable")) { reservationtable = data.getJSONObject("reservationtable"); } else { // reservations not specifically supported, let's just try it // with default values but fail silently reservationtable = new JSONObject(); reservationtable.put("author", 3); reservationtable.put("availability", 6); reservationtable.put("branch", -1); reservationtable.put("cancelurl", -1); reservationtable.put("expirationdate", 5); reservationtable.put("title", 3); } copymap = jsonToMap(reservationtable); } DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN); Elements rowElements = doc.select("form[name=vorml] table tr"); // rows: skip 1st row -> title row for (int i = 1; i < rowElements.size(); i++) { Element tr = rowElements.get(i); if (tr.child(0).tagName().equals("th")) { continue; } ReservedItem item = new ReservedItem(); item.setCancelData(tr.select("input[type=checkbox]").attr("name")); Elements mediatypeImg = tr.select("td img"); if (mediatypeImg.size() > 0) { item.setMediaType(getMediaTypeFromImageFilename( null, mediatypeImg.get(0).attr("src"), data)); } // columns: all elements of one media for (Map.Entry<String, Integer> entry : copymap.entrySet()) { String key = entry.getKey(); int index = entry.getValue(); String value = tr.child(index).text().trim(); switch (key) { case "author+title": item.setTitle(findTitleAndAuthor(value)[0]); item.setAuthor(findTitleAndAuthor(value)[1]); continue; case "availability": try { value = fmt.parseLocalDate(value).toString(); } catch (IllegalArgumentException e1) { key = "status"; } break; case "expirationdate": try { value = fmt.parseLocalDate(value).toString(); } catch (IllegalArgumentException e1) { key = "status"; } break; } if (value != null && value.length() != 0) { item.set(key, value); } } reservations.add(item); } return reservations; } /* * BiBer returns titles, authors and call numbers all in one field and cuts them of after a * fixed length. The exact formats differ greatly. * * Examples: * * Author: Title * Callnumber / Title * Callnumber / Author: Title * * Note that magazine titles might contain slashes as well, e.g. "Android Welt 3/15 Mai-Juni" */ private static Pattern PATTERN_TITLE_AUTHOR = Pattern.compile("(?:" + // Start matching the call number "[^/]+" + // The call number itself // A slash is only considered a separator between call number if it // isn't surrounded by digits (e.g. 2/12 in a magazine title) "(?<![0-9])/(?![0-9]{2})" + ")?" + // Signature is optional "(?:" + // Start matching the author "([^:]+)" + // The author itself // The author is separated form the title by a colon ":" + ")?" + // Author is optional // Everything else is considered to be part of the title "(.*)"); public static String[] findTitleAndAuthor(String value) { Matcher m = PATTERN_TITLE_AUTHOR.matcher(value); if (m.matches()) { return new String[]{m.group(2) != null ? m.group(2).trim() : null, m.group(1) != null ? m.group(1).trim() : null}; } else { return new String[]{null, null}; } } private Document accountHttpPost(Account account, String func) throws IOException, OpacErrorException { // get media list via http POST List<NameValuePair> nameValuePairs = new ArrayList<>(2); nameValuePairs.add(new BasicNameValuePair("FUNC", func)); nameValuePairs.add(new BasicNameValuePair("LANG", "de")); nameValuePairs .add(new BasicNameValuePair("BENUTZER", account.getName())); nameValuePairs.add(new BasicNameValuePair("PASSWORD", account .getPassword())); String html = httpPost(opacUrl + "/" + opacDir + "/user.C", new UrlEncodedFormEntity(nameValuePairs), getDefaultEncoding()); Document doc = Jsoup.parse(html); // Error recognition // <title>OPAC Fehler</title> if (doc.title().contains("Fehler") || (doc.select("h2").size() > 0 && doc.select("h2").text() .contains("Fehler"))) { String errText = "unknown error"; Elements elTable = doc.select("table"); if (elTable.size() > 0) { errText = elTable.get(0).text(); } throw new OpacErrorException(errText); } if (doc.select("tr td font[color=red]").size() == 1) { // Jena: Onleihe advertisement recognized as error message if (!doc.select("tr td font[color=red]").text() .contains("Ausleihe per Download rund um die Uhr")) { throw new OpacErrorException(doc.select("font[color=red]").text()); } } if (doc.text().contains("No html file set") || doc.text().contains("Der BIBDIA Server konnte den Auftrag nicht") || doc.text().contains("Fehler in der Ausf")) { throw new OpacErrorException( stringProvider.getString(StringProvider.WRONG_LOGIN_DATA)); } return doc; } @Override public String getShareUrl(String id, String title) { // id is normally full path like // "/opac/ftitle.C?LANG=de&FUNC=full&331313252=YES" // but sometimes (Wuerzburg) "ftitle.C?LANG=de&FUNC=full&331313252=YES" if (!id.startsWith("/")) { id = "/" + opacDir + "/" + id; } return opacUrl + id; } @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 public void checkAccountData(Account account) throws IOException, JSONException, OpacErrorException { Document doc = accountHttpPost(account, "medk"); if (doc == null) { throw new NotReachableException("Account document was null"); } } @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; } }