de.geeksfactory.opacclient.apis.TouchPoint.java Source code

Java tutorial

Introduction

Here is the source code for de.geeksfactory.opacclient.apis.TouchPoint.java

Source

/**
 * 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.select.Elements;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
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.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.DetailledItem;
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("bcher", 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("tontrger", 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;
    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> getSearchFields() 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);
        }

        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<>();

        int index = 0;
        start();

        params.add(new BasicNameValuePair("methodToCall", "submitButtonCall"));
        params.add(new BasicNameValuePair("CSId", CSId));
        params.add(new BasicNameValuePair("methodToCallParameter", "submitSearch"));
        params.add(new BasicNameValuePair("refine", "false"));

        for (SearchQuery entry : query) {
            if (entry.getValue().equals("")) {
                continue;
            }
            if (entry.getSearchField() instanceof DropdownSearchField) {
                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));
        }

        params.add(new BasicNameValuePair("submitButtonCall_submitSearch", "Suchen"));
        params.add(new BasicNameValuePair("numberOfHits", "10"));

        String html = httpGet(opac_url + "/search.do?" + URLEncodedUtils.format(params, "UTF-8"), ENCODING);
        return parse_search(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(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(html, page);
    }

    protected SearchRequestResult parse_search(String html, int page) throws OpacErrorException, IOException {
        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")) {
            reusehtml = html;
            throw new OpacErrorException("is_a_redirect");
        } 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("&amp;", "&")), 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 Mnchen
                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 mglich")
                            || loanstatus.contains("Keine Exemplare verfgbar"))) {
                        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 zurckgebucht"))
                            || (loanstatus.contains("ausleihbar") && !loanstatus.contains("nicht ausleihbar"))) {
                        sr.setStatus(SearchResult.Status.GREEN);
                    }
                    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 DetailledItem getResultById(String id, String homebranch) throws IOException {

        if (id == null && reusehtml != null) {
            DetailledItem r = parse_result(reusehtml);
            reusehtml = null;
            return r;
        }
        String html;
        try {
            JSONObject json = new JSONObject(id);
            html = httpGet(
                    opac_url + "/perma.do?q=" + URLEncoder.encode(
                            "0=\"" + json.getString("id") + "\" IN [" + json.getString("db") + "]", "UTF-8"),
                    ENCODING);
        } catch (JSONException e) {
            // backwards compatibility
            html = httpGet(opac_url + "/perma.do?q=" + URLEncoder.encode("0=\"" + id + "\" IN [2]", "UTF-8"),
                    ENCODING);
        }

        return parse_result(html);
    }

    @Override
    public DetailledItem getResult(int nr) throws IOException {
        if (reusehtml != null) {
            return getResultById(null, null);
        }
        String html = httpGet(
                opac_url + "/singleHit.do?methodToCall=showHit&curPos=" + nr + "&identifier=" + identifier,
                ENCODING);
        return parse_result(html);
    }

    protected DetailledItem parse_result(String html) throws IOException {
        Document doc = Jsoup.parse(html);
        doc.setBaseUri(opac_url);

        DetailledItem result = new DetailledItem();

        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());
        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.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("&amp;", "");
        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").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(DetailledItem item, Account acc, int useraction, String selection)
            throws IOException {
        if (System.currentTimeMillis() - logged_in > SESSION_LIFETIME || logged_in_as == null) {
            try {
                login(acc);
            } catch (OpacErrorException e) {
                return new ReservationResult(MultiStepResult.Status.ERROR, e.getMessage());
            }
        } else if (logged_in_as.getId() != acc.getId()) {
            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;
        }

        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 {
        return null;
    }

    @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-9A-Z]*(|EUR|CHF|Fr)\\)")) {
                text = text.replaceAll("Geb.+hren[^\\(]+\\(([0-9.,]+)[^0-9A-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.setAuthor(tr.select(".account-display-title").html().split("<br[ /]*>")[1].trim());

                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">Flligkeitsdatum : </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());

                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.get("methodToCall").equals("renewal")) {
                            item.setProlongData(href.split("\\?")[1]);
                            item.setRenewable(true);
                            break;
                        }
                    }
                }

            } catch (Exception ex) {
                ex.printStackTrace();
            }

            media.add(item);
        }
        return media;
    }

    static List<ReservedItem> parse_reslist(Document doc) {
        List<ReservedItem> reservations = new ArrayList<>();
        Elements copytrs = doc.select(".data 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].trim());
                if (rowsplit3.length > 2)
                    item.setBranch(rowsplit3[2].trim());
                if (rowsplit3.length > 2) {
                    item.setStatus(rowsplit3[0].trim() + " (" + rowsplit3[1].trim() + ")");
                }
            } 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 {
            try {
                JSONObject json = new JSONObject(id);
                return opac_url + "/perma.do?q=" + URLEncoder
                        .encode("0=\"" + json.getString("id") + "\" IN [" + json.getString("db") + "]", "UTF-8");
            } catch (JSONException e) {
                // backwards compatibility
                return opac_url + "/perma.do?q=" + URLEncoder.encode("0=\"" + id + "\" IN [2]", "UTF-8");
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public int getSupportFlags() {
        int flags = SUPPORT_FLAG_CHANGE_ACCOUNT;
        flags |= SUPPORT_FLAG_ENDLESS_SCROLLING;
        return flags;
    }

    @Override
    public ProlongAllResult prolongAll(Account account, int useraction, String selection) throws IOException {
        return null;
    }

    @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;
        }
    }
}