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

Java tutorial

Introduction

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

Source

/**
 * Copyright (C) 2013 by Raphael Michel under the MIT license:
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the Software 
 * is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */
package de.geeksfactory.opacclient.apis;

import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
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.URISyntaxException;
import java.net.URLEncoder;
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.apis.OpacApi.MultiStepResult.Status;
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.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 SISIS SunRise product, developed by OCLC.
 *
 * Restrictions: Bookmarks are only constantly supported if the library uses the BibTip extension.
 */
public class SISIS 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("Buch-Kinderbuch", 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.BOARDGAME);
        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("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 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("#tab-content select")) {
            parseDropdown(dropdown, fields);
        }

        return fields;
    }

    private void parseDropdown(Element dropdownElement, List<SearchField> fields) throws JSONException {
        Elements options = dropdownElement.select("option");
        DropdownSearchField dropdown = new DropdownSearchField();
        if (dropdownElement.parent().select("input[type=hidden]").size() > 0) {
            dropdown.setId(dropdownElement.parent().select("input[type=hidden]").attr("value"));
            dropdown.setData(new JSONObject("{\"restriction\": true}"));
        } else {
            dropdown.setId(dropdownElement.attr("name"));
            dropdown.setData(new JSONObject("{\"restriction\": false}"));
        }
        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;
        int restrictionIndex = 0;
        start();

        params.add(new BasicNameValuePair("methodToCall", "submit"));
        params.add(new BasicNameValuePair("CSId", CSId));
        params.add(new BasicNameValuePair("methodToCallParameter", "submitSearch"));

        for (SearchQuery entry : query) {
            if (entry.getValue().equals("")) {
                continue;
            }
            if (entry.getSearchField() instanceof DropdownSearchField) {
                JSONObject data = entry.getSearchField().getData();
                if (data.getBoolean("restriction")) {
                    params.add(new BasicNameValuePair("searchRestrictionID[" + restrictionIndex + "]",
                            entry.getSearchField().getId()));
                    params.add(new BasicNameValuePair("searchRestrictionValue1[" + restrictionIndex + "]",
                            entry.getValue()));
                    restrictionIndex++;
                } else {
                    params.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                }
            } else {
                if (index != 0) {
                    params.add(new BasicNameValuePair("combinationOperator[" + index + "]", "AND"));
                }
                params.add(new BasicNameValuePair("searchCategories[" + index + "]", entry.getKey()));
                params.add(new BasicNameValuePair("searchString[" + index + "]", entry.getValue()));
                index++;
            }
        }

        if (index == 0) {
            throw new OpacErrorException(stringProvider.getString(StringProvider.NO_CRITERIA_INPUT));
        }
        if (index > 4) {
            throw new OpacErrorException(
                    stringProvider.getQuantityString(StringProvider.LIMITED_NUM_OF_CRITERIA, 4, 4));
        }

        params.add(new BasicNameValuePair("submitSearch", "Suchen"));
        params.add(new BasicNameValuePair("callingPage", "searchParameters"));
        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 {
        Document doc = Jsoup.parse(html);
        doc.setBaseUri(opac_url + "/searchfoo");

        if (doc.select(".error").size() > 0) {
            throw new OpacErrorException(doc.select(".error").text().trim());
        } else if (doc.select(".nohits").size() > 0) {
            throw new OpacErrorException(doc.select(".nohits").text().trim());
        } else if (doc.select(".box-header h2, #nohits").text().contains("keine Treffer")) {
            return new SearchRequestResult(new ArrayList<SearchResult>(), 0, 1, 1);
        }

        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 (int i = 0; i < links.size(); i++) {
            Element node = links.get(i);
            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("td img[title]").size() > 0) {
                String title = tr.select("td img").get(0).attr("title");
                String[] fparts = tr.select("td img").get(0).attr("src").split("/");
                String fname = fparts[fparts.length - 1];
                MediaType default_by_fname = defaulttypes.get(fname.toLowerCase(Locale.GERMAN).replace(".jpg", "")
                        .replace(".gif", "").replace(".png", ""));
                MediaType default_by_title = defaulttypes.get(title);
                MediaType default_name = default_by_title != null ? default_by_title : default_by_fname;
                if (data.has("mediatypes")) {
                    try {
                        sr.setType(MediaType.valueOf(data.getJSONObject("mediatypes").getString(fname)));
                    } catch (JSONException | IllegalArgumentException e) {
                        sr.setType(default_name);
                    }
                } else {
                    sr.setType(default_name);
                }
            }
            String alltext = tr.text();
            if (alltext.contains("eAudio") || alltext.contains("eMusic")) {
                sr.setType(MediaType.MP3);
            } else if (alltext.contains("eVideo")) {
                sr.setType(MediaType.EVIDEO);
            } else if (alltext.contains("eBook")) {
                sr.setType(MediaType.EBOOK);
            } else if (alltext.contains("Munzinger")) {
                sr.setType(MediaType.EDOC);
            }

            if (tr.children().size() > 3 && tr.child(3).select("img[title*=cover]").size() == 1) {
                sr.setCover(tr.child(3).select("img[title*=cover]").attr("abs:src"));
                if (sr.getCover().contains("showCover.do")) {
                    downloadCover(sr);
                }
            }

            Element middlething;
            if (tr.children().size() > 2 && tr.child(2).select("a").size() > 0) {
                middlething = tr.child(2);
            } else {
                middlething = tr.child(1);
            }

            List<Node> children = middlething.childNodes();
            if (middlething.select("div").not("#hlrightblock,.bestellfunktionen").size() == 1) {
                Element indiv = middlething.select("div").not("#hlrightblock,.bestellfunktionen").first();
                if (indiv.children().size() > 1) {
                    children = indiv.childNodes();
                }
            } else if (middlething.select("span.titleData").size() == 1) {
                children = middlething.select("span.titleData").first().childNodes();
            }
            int childrennum = children.size();

            List<String[]> strings = new ArrayList<>();
            for (int ch = 0; ch < childrennum; ch++) {
                Node node = children.get(ch);
                if (node instanceof TextNode) {
                    String text = ((TextNode) node).text().trim();
                    if (text.length() > 3) {
                        strings.add(new String[] { "text", "", text });
                    }
                } else if (node instanceof Element) {

                    List<Node> subchildren = node.childNodes();
                    for (int j = 0; j < subchildren.size(); j++) {
                        Node subnode = subchildren.get(j);
                        if (subnode instanceof TextNode) {
                            String text = ((TextNode) subnode).text().trim();
                            if (text.length() > 3) {
                                strings.add(new String[] { ((Element) node).tag().getName(), "text", text,
                                        ((Element) node).className(), node.attr("style") });
                            }
                        } else if (subnode instanceof Element) {
                            String text = ((Element) subnode).text().trim();
                            if (text.length() > 3) {
                                strings.add(new String[] { ((Element) node).tag().getName(),
                                        ((Element) subnode).tag().getName(), text, ((Element) node).className(),
                                        node.attr("style") });
                            }
                        }
                    }
                }
            }

            StringBuilder description = null;
            if (tr.select("span.Z3988").size() == 1) {
                // Sometimes there is a <span class="Z3988"> item which provides
                // data in a standardized format.
                List<NameValuePair> z3988data;
                boolean hastitle = false;
                try {
                    description = new StringBuilder();
                    z3988data = URLEncodedUtils
                            .parse(new URI("http://dummy/?" + tr.select("span.Z3988").attr("title")), "UTF-8");
                    for (NameValuePair nv : z3988data) {
                        if (nv.getValue() != null) {
                            if (!nv.getValue().trim().equals("")) {
                                if (nv.getName().equals("rft.btitle") && !hastitle) {
                                    description.append("<b>").append(nv.getValue()).append("</b>");
                                    hastitle = true;
                                } else if (nv.getName().equals("rft.atitle") && !hastitle) {
                                    description.append("<b>").append(nv.getValue()).append("</b>");
                                    hastitle = true;
                                } else if (nv.getName().equals("rft.au")) {
                                    description.append("<br />").append(nv.getValue());
                                } else if (nv.getName().equals("rft.date")) {
                                    description.append("<br />").append(nv.getValue());
                                }
                            }
                        }
                    }
                } catch (URISyntaxException e) {
                    description = null;
                }
            }
            boolean described = false;
            if (description != null && description.length() > 0) {
                sr.setInnerhtml(description.toString());
                described = true;
            } else {
                description = new StringBuilder();
            }
            int k = 0;
            boolean yearfound = false;
            boolean titlefound = false;
            boolean sigfound = false;
            for (String[] part : strings) {
                if (!described) {
                    if (part[0].equals("a") && (k == 0 || !titlefound)) {
                        if (k != 0) {
                            description.append("<br />");
                        }
                        description.append("<b>").append(part[2]).append("</b>");
                        titlefound = true;
                    } else if (part[2].matches("\\D*[0-9]{4}\\D*") && part[2].length() <= 10) {
                        yearfound = true;
                        if (k != 0) {
                            description.append("<br />");
                        }
                        description.append(part[2]);
                    } else if (k == 1 && !yearfound && part[2].matches("^\\s*\\([0-9]{4}\\)$")) {
                        if (k != 0) {
                            description.append("<br />");
                        }
                        description.append(part[2]);
                    } else if (k == 1 && !yearfound && part[2].matches("^\\s*\\([0-9]{4}\\)$")) {
                        if (k != 0) {
                            description.append("<br />");
                        }
                        description.append(part[2]);
                    } else if (k > 1 && k < 4 && !sigfound && part[0].equals("text")
                            && part[2].matches("^[A-Za-z0-9,\\- ]+$")) {
                        description.append("<br />");
                        description.append(part[2]);
                    }
                }
                if (part.length == 4) {
                    if (part[0].equals("span") && part[3].equals("textgruen")) {
                        sr.setStatus(SearchResult.Status.GREEN);
                    } else if (part[0].equals("span") && part[3].equals("textrot")) {
                        sr.setStatus(SearchResult.Status.RED);
                    }
                } else if (part.length == 5) {
                    if (part[4].contains("purple")) {
                        sr.setStatus(SearchResult.Status.YELLOW);
                    }
                }
                if (sr.getStatus() == null) {
                    if ((part[2].contains("entliehen")
                            && part[2].startsWith("Vormerkung ist leider nicht mglich"))
                            || part[2].contains("nur in anderer Zweigstelle ausleihbar und nicht bestellbar")) {
                        sr.setStatus(SearchResult.Status.RED);
                    } else if (part[2].startsWith("entliehen")
                            || part[2].contains("Ein Exemplar finden Sie in einer anderen Zweigstelle")) {
                        sr.setStatus(SearchResult.Status.YELLOW);
                    } else if ((part[2].startsWith("bestellbar") && !part[2].contains("nicht bestellbar"))
                            || (part[2].startsWith("vorbestellbar") && !part[2].contains("nicht vorbestellbar"))
                            || (part[2].startsWith("vorbestellbar") && !part[2].contains("nicht vorbestellbar"))
                            || (part[2].startsWith("vormerkbar") && !part[2].contains("nicht vormerkbar"))
                            || (part[2].contains("heute zurckgebucht"))
                            || (part[2].contains("ausleihbar") && !part[2].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);
                        }
                    }
                }
                k++;
            }
            if (!described) {
                sr.setInnerhtml(description.toString());
            }

            sr.setNr(10 * (page - 1) + i);
            sr.setId(null);
            results.add(sr);
        }
        resultcount = results.size();
        return new SearchRequestResult(results, results_total, page);
    }

    @Override
    public DetailledItem getResultById(String id, String homebranch) throws IOException {

        if (id == null && reusehtml != null) {
            DetailledItem r = parse_result(reusehtml);
            reusehtml = null;
            return r;
        }

        // 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 hbp = "";
        if (homebranch != null) {
            hbp = "&selectedViewBranchlib=" + homebranch;
        }

        String html = httpGet(
                opac_url + "/start.do?" + startparams + "searchType=1&Query=0%3D%22" + id + "%22" + hbp, 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?tab=showExemplarActive&methodToCall=showHit&curPos="
                + (nr + 1) + "&identifier=" + identifier, ENCODING);

        return parse_result(html);
    }

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

        String html2 = httpGet(opac_url + "/singleHit.do?methodToCall=activateTab&tab=showTitleActive", ENCODING);

        Document doc2 = Jsoup.parse(html2);
        doc2.setBaseUri(opac_url);

        String html3 = httpGet(opac_url + "/singleHit.do?methodToCall=activateTab&tab=showAvailabilityActive",
                ENCODING);

        Document doc3 = Jsoup.parse(html3);
        doc3.setBaseUri(opac_url);

        DetailledItem result = new DetailledItem();

        try {
            result.setId(doc.select("#bibtip_id").text().trim());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        List<String> reservationlinks = new ArrayList<>();
        for (Element link : doc3.select("#vormerkung a, #tab-content a")) {
            String href = link.absUrl("href");
            Map<String, String> hrefq = getQueryParamsFirst(href);
            if (result.getId() == null) {
                // ID retrieval
                String key = hrefq.get("katkey");
                if (key != null) {
                    result.setId(key);
                    break;
                }
            }

            // Vormerken
            if (hrefq.get("methodToCall") != null) {
                if (hrefq.get("methodToCall").equals("doVormerkung")
                        || hrefq.get("methodToCall").equals("doBestellung")) {
                    reservationlinks.add(href.split("\\?")[1]);
                }
            }
        }
        if (reservationlinks.size() == 1) {
            result.setReservable(true);
            result.setReservation_info(reservationlinks.get(0));
        } else if (reservationlinks.size() == 0) {
            result.setReservable(false);
        } else {
            // TODO: Multiple options - handle this case!
        }

        if (doc.select(".data td img").size() == 1) {
            result.setCover(doc.select(".data td img").first().attr("abs:src"));
            try {
                downloadCover(result);
            } catch (Exception e) {

            }
        }

        if (doc.select(".aw_teaser_title").size() == 1) {
            result.setTitle(doc.select(".aw_teaser_title").first().text().trim());
        } else if (doc.select(".data td strong").size() > 0) {
            result.setTitle(doc.select(".data td strong").first().text().trim());
        } else {
            result.setTitle("");
        }
        if (doc.select(".aw_teaser_title_zusatz").size() > 0) {
            result.addDetail(new Detail("Titelzusatz", doc.select(".aw_teaser_title_zusatz").text().trim()));
        }

        String title = "";
        String text = "";
        boolean takeover = false;
        Element detailtrs = doc2.select(".box-container .data td").first();
        for (Node node : detailtrs.childNodes()) {
            if (node instanceof Element) {
                if (((Element) node).tagName().equals("strong")) {
                    title = ((Element) node).text().trim();
                    text = "";
                } else {
                    if (((Element) node).tagName().equals("a")
                            && (((Element) node).text().trim().contains("hier klicken") || title.equals("Link:"))) {
                        text = text + node.attr("href");
                        takeover = true;
                        break;
                    }
                }
            } else if (node instanceof TextNode) {
                text = text + ((TextNode) node).text();
            }
        }
        if (!takeover) {
            text = "";
            title = "";
        }

        detailtrs = doc2.select("#tab-content .data td").first();
        if (detailtrs != null) {
            for (Node node : detailtrs.childNodes()) {
                if (node instanceof Element) {
                    if (((Element) node).tagName().equals("strong")) {
                        if (!text.equals("") && !title.equals("")) {
                            result.addDetail(new Detail(title.trim(), text.trim()));
                            if (title.equals("Titel:")) {
                                result.setTitle(text.trim());
                            }
                            text = "";
                        }

                        title = ((Element) node).text().trim();
                    } else {
                        if (((Element) node).tagName().equals("a")
                                && (((Element) node).text().trim().contains("hier klicken")
                                        || title.equals("Link:"))) {
                            text = text + node.attr("href");
                        } else {
                            text = text + ((Element) node).text();
                        }
                    }
                } else if (node instanceof TextNode) {
                    text = text + ((TextNode) node).text();
                }
            }
        } else {
            if (doc2.select("#tab-content .fulltitle tr").size() > 0) {
                Elements rows = doc2.select("#tab-content .fulltitle tr");
                for (Element tr : rows) {
                    if (tr.children().size() == 2) {
                        Element valcell = tr.child(1);
                        String value = valcell.text().trim();
                        if (valcell.select("a").size() == 1) {
                            value = valcell.select("a").first().absUrl("href");
                        }
                        result.addDetail(new Detail(tr.child(0).text().trim(), value));
                    }
                }
            } else {
                result.addDetail(new Detail(stringProvider.getString(StringProvider.ERROR),
                        stringProvider.getString(StringProvider.COULD_NOT_LOAD_DETAIL)));
            }
        }
        if (!text.equals("") && !title.equals("")) {
            result.addDetail(new Detail(title.trim(), text.trim()));
            if (title.equals("Titel:")) {
                result.setTitle(text.trim());
            }
        }
        for (Element link : doc3.select("#tab-content a")) {
            Map<String, String> hrefq = getQueryParamsFirst(link.absUrl("href"));
            if (result.getId() == null) {
                // ID retrieval
                String key = hrefq.get("katkey");
                if (key != null) {
                    result.setId(key);
                    break;
                }
            }
        }
        for (Element link : doc3.select(".box-container a")) {
            if (link.text().trim().equals("Download")) {
                result.addDetail(
                        new Detail(stringProvider.getString(StringProvider.DOWNLOAD), link.absUrl("href")));
            }
        }

        Map<String, Integer> copy_columnmap = new HashMap<>();
        // Default values
        copy_columnmap.put("barcode", 1);
        copy_columnmap.put("branch", 3);
        copy_columnmap.put("status", 4);
        Elements copy_columns = doc.select("#tab-content .data tr#bg2 th");
        for (int i = 0; i < copy_columns.size(); i++) {
            Element th = copy_columns.get(i);
            String head = th.text().trim();
            if (head.contains("Status")) {
                copy_columnmap.put("status", i);
            }
            if (head.contains("Zweigstelle")) {
                copy_columnmap.put("branch", i);
            }
            if (head.contains("Mediennummer")) {
                copy_columnmap.put("barcode", i);
            }
            if (head.contains("Standort")) {
                copy_columnmap.put("location", i);
            }
            if (head.contains("Signatur")) {
                copy_columnmap.put("signature", i);
            }
        }

        Pattern status_lent = Pattern.compile(
                "^(entliehen) bis ([0-9]{1,2}.[0-9]{1,2}.[0-9]{2," + "4}) \\(gesamte Vormerkungen: ([0-9]+)\\)$");
        Pattern status_and_barcode = Pattern.compile("^(.*) ([0-9A-Za-z]+)$");

        Elements exemplartrs = doc.select("#tab-content .data tr").not("#bg2");
        DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN);
        for (Element tr : exemplartrs) {
            try {
                Copy copy = new Copy();
                Element status = tr.child(copy_columnmap.get("status"));
                Element barcode = tr.child(copy_columnmap.get("barcode"));
                String barcodetext = barcode.text().trim().replace(" Wegweiser", "");

                // STATUS
                String statustext;
                if (status.getElementsByTag("b").size() > 0) {
                    statustext = status.getElementsByTag("b").text().trim();
                } else {
                    statustext = status.text().trim();
                }
                if (copy_columnmap.get("status").equals(copy_columnmap.get("barcode"))) {
                    Matcher matcher1 = status_and_barcode.matcher(statustext);
                    if (matcher1.matches()) {
                        statustext = matcher1.group(1);
                        barcodetext = matcher1.group(2);
                    }
                }

                Matcher matcher = status_lent.matcher(statustext);
                if (matcher.matches()) {
                    copy.setStatus(matcher.group(1));
                    copy.setReservations(matcher.group(3));
                    copy.setReturnDate(fmt.parseLocalDate(matcher.group(2)));
                } else {
                    copy.setStatus(statustext);
                }
                copy.setBarcode(barcodetext);
                if (status.select("a[href*=doVormerkung]").size() == 1) {
                    copy.setResInfo(status.select("a[href*=doVormerkung]").attr("href").split("\\?")[1]);
                }

                String branchtext = tr.child(copy_columnmap.get("branch")).text().trim().replace(" Wegweiser", "");
                copy.setBranch(branchtext);

                if (copy_columnmap.containsKey("location")) {
                    copy.setLocation(
                            tr.child(copy_columnmap.get("location")).text().trim().replace(" Wegweiser", ""));
                }

                if (copy_columnmap.containsKey("signature")) {
                    copy.setShelfmark(
                            tr.child(copy_columnmap.get("signature")).text().trim().replace(" Wegweiser", ""));
                }

                result.addCopy(copy);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        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 {
        String reservation_info = item.getReservation_info();
        final String branch_inputfield = "issuepoint";

        Document doc = null;

        String action = "reservation";
        if (reservation_info.contains("doBestellung")) {
            action = "order";
        }

        if (useraction == MultiStepResult.ACTION_CONFIRMATION) {
            List<NameValuePair> nameValuePairs = new ArrayList<>(2);
            nameValuePairs.add(new BasicNameValuePair("methodToCall", action));
            nameValuePairs.add(new BasicNameValuePair("CSId", CSId));
            String html = httpPost(opac_url + "/" + action + ".do", new UrlEncodedFormEntity(nameValuePairs),
                    ENCODING);
            doc = Jsoup.parse(html);
        } else if (selection == null || useraction == 0) {
            String html = httpGet(opac_url + "/availability.do?" + reservation_info, ENCODING);
            doc = Jsoup.parse(html);

            if (doc.select("input[name=username]").size() > 0) {
                // Login vonnten
                List<NameValuePair> nameValuePairs = new ArrayList<>(2);
                nameValuePairs.add(new BasicNameValuePair("username", acc.getName()));
                nameValuePairs.add(new BasicNameValuePair("password", acc.getPassword()));
                nameValuePairs.add(new BasicNameValuePair("methodToCall", "submit"));
                nameValuePairs.add(new BasicNameValuePair("CSId", CSId));
                nameValuePairs.add(new BasicNameValuePair("login_action", "Login"));

                html = handleLoginMessage(
                        httpPost(opac_url + "/login.do", new UrlEncodedFormEntity(nameValuePairs), ENCODING));
                doc = Jsoup.parse(html);

                if (doc.getElementsByClass("error").size() == 0) {
                    logged_in = System.currentTimeMillis();
                    logged_in_as = acc;
                }
            }
            if (doc.select("input[name=expressorder]").size() > 0) {
                List<NameValuePair> nameValuePairs = new ArrayList<>(2);
                nameValuePairs.add(new BasicNameValuePair(branch_inputfield, selection));
                nameValuePairs.add(new BasicNameValuePair("methodToCall", action));
                nameValuePairs.add(new BasicNameValuePair("CSId", CSId));
                nameValuePairs.add(new BasicNameValuePair("expressorder", " "));
                html = httpPost(opac_url + "/" + action + ".do", new UrlEncodedFormEntity(nameValuePairs),
                        ENCODING);
                doc = Jsoup.parse(html);
            }
            if (doc.select("input[name=" + branch_inputfield + "]").size() > 0) {
                List<Map<String, String>> branches = new ArrayList<>();
                for (Element option : doc.select("input[name=" + branch_inputfield + "]").first().parent().parent()
                        .parent().select("td")) {
                    if (option.select("input").size() != 1) {
                        continue;
                    }
                    String value = option.text().trim();
                    String key = option.select("input").val();
                    Map<String, String> selopt = new HashMap<>();
                    selopt.put("key", key);
                    selopt.put("value", value);
                    branches.add(selopt);
                }
                ReservationResult result = new ReservationResult(MultiStepResult.Status.SELECTION_NEEDED);
                result.setActionIdentifier(ReservationResult.ACTION_BRANCH);
                result.setSelection(branches);
                return result;
            }
        } else if (useraction == ReservationResult.ACTION_BRANCH) {
            List<NameValuePair> nameValuePairs = new ArrayList<>(2);
            nameValuePairs.add(new BasicNameValuePair(branch_inputfield, selection));
            nameValuePairs.add(new BasicNameValuePair("methodToCall", action));
            nameValuePairs.add(new BasicNameValuePair("CSId", CSId));

            String html = httpPost(opac_url + "/" + action + ".do", new UrlEncodedFormEntity(nameValuePairs),
                    ENCODING);
            doc = Jsoup.parse(html);
        }

        if (doc == null) {
            return new ReservationResult(MultiStepResult.Status.ERROR);
        }

        if (doc.getElementsByClass("error").size() >= 1) {
            return new ReservationResult(MultiStepResult.Status.ERROR,
                    doc.getElementsByClass("error").get(0).text());
        }

        if (doc.select("#CirculationForm p").size() > 0 && doc.select("input[type=button]").size() >= 2) {
            List<String[]> details = new ArrayList<>();
            for (String row : doc.select("#CirculationForm p").first().html().split("<br>")) {
                Document frag = Jsoup.parseBodyFragment(row);
                if (frag.text().contains(":")) {
                    String[] split = frag.text().split(":");
                    if (split.length >= 2) {
                        details.add(new String[] { split[0].trim() + ":", split[1].trim() });
                    }
                } else {
                    details.add(new String[] { "", frag.text().trim() });
                }
            }
            ReservationResult result = new ReservationResult(Status.CONFIRMATION_NEEDED);
            result.setDetails(details);
            return result;
        }

        if (doc.select("#CirculationForm .textrot").size() >= 1) {
            String errmsg = doc.select("#CirculationForm .textrot").get(0).text();
            if (errmsg.contains("Dieses oder andere Exemplare in anderer Zweigstelle ausleihbar")) {
                Copy best = null;
                for (Copy copy : item.getCopies()) {
                    if (copy.getResInfo() == null) {
                        continue;
                    }
                    if (best == null) {
                        best = copy;
                        continue;
                    }
                    try {
                        if (Integer.parseInt(copy.getReservations()) < Long.parseLong(best.getReservations())) {
                            best = copy;
                        } else if (Integer.parseInt(copy.getReservations()) == Long
                                .parseLong(best.getReservations())) {
                            if (copy.getReturnDate().isBefore(best.getReturnDate())) {
                                best = copy;
                            }
                        }
                    } catch (NumberFormatException e) {

                    }
                }
                if (best != null) {
                    item.setReservation_info(best.getResInfo());
                    return reservation(item, acc, 0, null);
                }
            }
            return new ReservationResult(MultiStepResult.Status.ERROR, errmsg);
        }

        if (doc.select("#CirculationForm td[colspan=2] strong").size() >= 1) {
            return new ReservationResult(MultiStepResult.Status.OK,
                    doc.select("#CirculationForm td[colspan=2] strong").get(0).text());
        }
        return new ReservationResult(Status.OK);
    }

    @Override
    public ProlongResult prolong(String a, Account account, int useraction, String Selection) throws IOException {
        // Internal convention: a is either a  followed by an error message or
        // the URI of the page this item was found on and the query string the
        // prolonging link links to, seperated by a $.
        if (a.startsWith("")) {
            return new ProlongResult(MultiStepResult.Status.ERROR, a.substring(1));
        }
        String[] parts = a.split("\\$");
        String offset = parts[0];
        String query = parts[1];

        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 originally found the link on first...
        httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&typ=1", ENCODING);
        if (!offset.equals("1")) {
            httpGet(opac_url + "/userAccount.do?methodToCall=pos&accountTyp=AUSLEIHEN&anzPos=" + offset, ENCODING);
        }
        String html = httpGet(opac_url + "/userAccount.do?" + query, ENCODING);
        Document doc = Jsoup.parse(html);
        if (doc.select("#middle .textrot").size() > 0) {
            return new ProlongResult(MultiStepResult.Status.ERROR, doc.select("#middle .textrot").first().text());
        }

        return new ProlongResult(MultiStepResult.Status.OK);
    }

    @Override
    public CancelResult cancel(String media, Account account, int useraction, String selection)
            throws IOException, OpacErrorException {
        if (!initialised) {
            start();
        }

        String[] parts = media.split("\\$");
        String type = parts[0];
        String offset = parts[1];
        String query = parts[2];

        if (System.currentTimeMillis() - logged_in > SESSION_LIFETIME || logged_in_as == null) {
            try {
                account(account);
            } catch (JSONException e) {
                e.printStackTrace();
                throw new OpacErrorException(stringProvider.getString(StringProvider.INTERNAL_ERROR));
            }
        } else if (logged_in_as.getId() != account.getId()) {
            try {
                account(account);
            } catch (JSONException e) {
                e.printStackTrace();
                throw new OpacErrorException(stringProvider.getString(StringProvider.INTERNAL_ERROR));
            }
        }

        // We have to call the page we originally found the link on first...
        httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&typ=" + type, ENCODING);
        if (!offset.equals("1")) {
            httpGet(opac_url + "/userAccount.do?methodToCall=pos&anzPos=" + offset, ENCODING);
        }
        httpGet(opac_url + "/userAccount.do?" + query, ENCODING);
        return new CancelResult(MultiStepResult.Status.OK);
    }

    protected String handleLoginMessage(String html) throws IOException {
        if (html.contains("methodToCall=done")) {
            return httpGet(opac_url + "/login.do?methodToCall=done", ENCODING);
        } else {
            return html;
        }
    }

    protected boolean login(Account acc) throws OpacErrorException {
        String html;

        List<NameValuePair> nameValuePairs = new ArrayList<>(2);

        try {
            String loginPage;
            loginPage = httpGet(opac_url + "/userAccount.do?methodToCall=show&type=1", ENCODING);
            Document loginPageDoc = Jsoup.parse(loginPage);
            if (loginPageDoc.select("input[name=as_fid]").size() > 0) {
                nameValuePairs.add(new BasicNameValuePair("as_fid",
                        loginPageDoc.select("input[name=as_fid]").first().attr("value")));
            }
        } 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"));
        try {
            html = handleLoginMessage(
                    httpPost(opac_url + "/login.do", new UrlEncodedFormEntity(nameValuePairs), ENCODING));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return false;
        } catch (ClientProtocolException e) {
            e.printStackTrace();
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        Document doc = Jsoup.parse(html);

        if (doc.getElementsByClass("error").size() > 0) {
            throw new OpacErrorException(doc.getElementsByClass("error").get(0).text());
        }

        logged_in = System.currentTimeMillis();
        logged_in_as = acc;

        return true;
    }

    protected void parse_medialist(List<LentItem> media, Document doc, int offset) {
        Elements copytrs = doc.select(".data tr");
        doc.setBaseUri(opac_url);

        DateTimeFormatter fmt = DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN);

        int trs = copytrs.size();
        if (trs == 1) {
            return;
        }
        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;
            }

            item.setTitle(tr.child(1).select("strong").text().trim());
            try {
                item.setAuthor(tr.child(1).html().split("<br[ /]*>")[1].trim());

                String[] col2split = tr.child(2).html().split("<br[ /]*>");
                String deadline = col2split[0].trim();
                if (deadline.contains("-")) {
                    deadline = deadline.split("-")[1].trim();
                }
                try {
                    item.setDeadline(fmt.parseLocalDate(deadline).toString());
                } catch (IllegalArgumentException e1) {
                    e1.printStackTrace();
                }

                if (col2split.length > 1) {
                    item.setHomeBranch(col2split[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("renewalPossible")) {
                            item.setProlongData(offset + "$" + href.split("\\?")[1]);
                            item.setRenewable(true);
                            break;
                        }
                    }
                } else if (tr.select(".textrot, .textgruen, .textdunkelblau").size() > 0) {
                    item.setProlongData("" + tr.select(".textrot, .textgruen, .textdunkelblau").text());
                    item.setRenewable(false);
                }

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

            media.add(item);
        }
        assert (media.size() == trs - 1);

    }

    protected void parse_reslist(String type, List<ReservedItem> reservations, Document doc, int offset) {
        Elements copytrs = doc.select(".data tr");
        doc.setBaseUri(opac_url);
        int trs = copytrs.size();
        if (trs == 1) {
            return;
        }
        assert (trs > 0);
        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;
            }

            item.setTitle(tr.child(1).select("strong").text().trim());
            try {
                String[] rowsplit1 = tr.child(1).html().split("<br[ /]*>");
                String[] rowsplit2 = tr.child(2).html().split("<br[ /]*>");
                if (rowsplit1.length > 1)
                    item.setAuthor(rowsplit1[1].trim());
                if (rowsplit2.length > 2)
                    item.setBranch(rowsplit2[2].trim());
                if (rowsplit2.length > 2)
                    item.setStatus(rowsplit2[0].trim());

                if (tr.select("a").size() == 1) {
                    item.setCancelData(type + "$" + offset + "$" + tr.select("a").attr("abs:href").split("\\?")[1]);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            reservations.add(item);
        }
        assert (reservations.size() == trs - 1);
    }

    @Override
    public AccountData account(Account acc) throws IOException, JSONException, OpacErrorException {
        start(); // TODO: Is this necessary?

        int resultNum;

        if (!login(acc)) {
            return null;
        }

        // Geliehene Medien
        String html = httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&typ=1", ENCODING);
        List<LentItem> medien = new ArrayList<>();
        Document doc = Jsoup.parse(html);
        doc.setBaseUri(opac_url);
        parse_medialist(medien, doc, 1);
        if (doc.select(".box-right").size() > 0) {
            for (Element link : doc.select(".box-right").first().select("a")) {
                String href = link.attr("abs:href");
                Map<String, String> hrefq = getQueryParamsFirst(href);
                if (hrefq == null || hrefq.get("methodToCall") == null) {
                    continue;
                }
                if (hrefq.get("methodToCall").equals("pos") && !"1".equals(hrefq.get("anzPos"))) {
                    html = httpGet(href, ENCODING);
                    parse_medialist(medien, Jsoup.parse(html), Integer.parseInt(hrefq.get("anzPos")));
                }
            }
        }
        if (doc.select("#label1").size() > 0) {
            resultNum = 0;
            String rNum = doc.select("#label1").first().text().trim().replaceAll(".*\\(([0-9]*)\\).*", "$1");
            if (rNum.length() > 0) {
                resultNum = Integer.parseInt(rNum);
            }

            assert (resultNum == medien.size());
        }

        // Ordered media ("Bestellungen")
        html = httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&typ=6", ENCODING);
        List<ReservedItem> reserved = new ArrayList<>();
        doc = Jsoup.parse(html);
        doc.setBaseUri(opac_url);
        parse_reslist("6", reserved, doc, 1);
        Elements label6 = doc.select("#label6");
        if (doc.select(".box-right").size() > 0) {
            for (Element link : doc.select(".box-right").first().select("a")) {
                String href = link.attr("abs:href");
                Map<String, String> hrefq = getQueryParamsFirst(href);
                if (hrefq == null || hrefq.get("methodToCall") == null) {
                    break;
                }
                if (hrefq.get("methodToCall").equals("pos") && !"1".equals(hrefq.get("anzPos"))) {
                    html = httpGet(href, ENCODING);
                    parse_reslist("6", reserved, Jsoup.parse(html), Integer.parseInt(hrefq.get("anzPos")));
                }
            }
        }

        // Prebooked media ("Vormerkungen")
        html = httpGet(opac_url + "/userAccount.do?methodToCall=showAccount&typ=7", ENCODING);
        doc = Jsoup.parse(html);
        doc.setBaseUri(opac_url);
        parse_reslist("7", reserved, doc, 1);
        if (doc.select(".box-right").size() > 0) {
            for (Element link : doc.select(".box-right").first().select("a")) {
                String href = link.attr("abs:href");
                Map<String, String> hrefq = getQueryParamsFirst(href);
                if (hrefq == null || hrefq.get("methodToCall") == null) {
                    break;
                }
                if (hrefq.get("methodToCall").equals("pos") && !"1".equals(hrefq.get("anzPos"))) {
                    html = httpGet(href, ENCODING);
                    parse_reslist("7", reserved, Jsoup.parse(html), Integer.parseInt(hrefq.get("anzPos")));
                }
            }
        }
        if (label6.size() > 0 && doc.select("#label7").size() > 0) {
            resultNum = 0;
            String rNum = label6.text().trim().replaceAll(".*\\(([0-9]*)\\).*", "$1");
            if (rNum.length() > 0) {
                resultNum = Integer.parseInt(rNum);
            }
            rNum = doc.select("#label7").text().trim().replaceAll(".*\\(([0-9]*)\\).*", "$1");
            if (rNum.length() > 0) {
                resultNum += Integer.parseInt(rNum);
            }
            assert (resultNum == reserved.size());
        }

        AccountData res = new AccountData(acc.getId());

        if (doc.select("#label8").size() > 0) {
            String text = doc.select("#label8").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");
                res.setPendingFees(text);
            }
        }
        Pattern p = Pattern.compile("[^0-9.]*", Pattern.MULTILINE);
        if (doc.select(".box3").size() > 0) {
            for (Element box : doc.select(".box3")) {
                if (box.select("strong").size() == 1) {
                    String text = box.select("strong").text();
                    if (text.equals("Jahresgebhren")) {
                        text = box.text();
                        text = p.matcher(text).replaceAll("");
                        res.setValidUntil(text);
                    }
                }

            }
        }

        res.setLent(medien);
        res.setReservations(reserved);
        return res;
    }

    @Override
    public String getShareUrl(String id, String title) {
        String startparams = "";
        if (data.has("startparams")) {
            try {
                startparams = data.getString("startparams") + "&";
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        if (id != null && !id.equals("")) {
            return opac_url + "/start.do?" + startparams + "searchType=1&Query=0%3D%22" + id + "%22";
        } else {
            try {
                title = URLEncoder.encode(title, getDefaultEncoding());
            } catch (UnsupportedEncodingException e) {
                //noinspection deprecation
                title = URLEncoder.encode(title);
            }
            return opac_url + "/start.do?" + startparams + "searchType=1&Query=-1%3D%22" + title + "%22";
        }
    }

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

    @Override
    public ProlongAllResult prolongAll(Account account, int useraction, String selection) throws IOException {
        if (!initialised) {
            start();
        }
        if (System.currentTimeMillis() - logged_in > SESSION_LIFETIME || logged_in_as == null) {
            try {
                account(account);
            } catch (JSONException e) {
                e.printStackTrace();
                return new ProlongAllResult(MultiStepResult.Status.ERROR);
            } catch (OpacErrorException e) {
                return new ProlongAllResult(MultiStepResult.Status.ERROR, e.getMessage());
            }
        } else if (logged_in_as.getId() != account.getId()) {
            try {
                account(account);
            } catch (JSONException e) {
                e.printStackTrace();
                return new ProlongAllResult(MultiStepResult.Status.ERROR);
            } catch (OpacErrorException e) {
                return new ProlongAllResult(MultiStepResult.Status.ERROR, e.getMessage());
            }
        }

        // We have to call the page we originally found the link on first...
        String html = httpGet(opac_url + "/userAccount.do?methodToCall=renewalPossible&renewal=account", ENCODING);
        Document doc = Jsoup.parse(html);

        if (doc.select("table.data").size() > 0) {
            List<Map<String, String>> result = new ArrayList<>();
            for (Element td : doc.select("table.data tr td")) {
                Map<String, String> line = new HashMap<>();
                if (!td.text().contains("Titel") || !td.text().contains("Status")) {
                    continue;
                }
                String nextNodeIs = "";
                for (Node n : td.childNodes()) {
                    String text;
                    if (n instanceof Element) {
                        text = ((Element) n).text();
                    } else if (n instanceof TextNode) {
                        text = ((TextNode) n).text();
                    } else {
                        continue;
                    }
                    if (text.trim().length() == 0) {
                        continue;
                    }
                    if (text.contains("Titel:")) {
                        nextNodeIs = ProlongAllResult.KEY_LINE_TITLE;
                    } else if (text.contains("Verfasser:")) {
                        nextNodeIs = ProlongAllResult.KEY_LINE_AUTHOR;
                    } else if (text.contains("Leihfristende:")) {
                        nextNodeIs = ProlongAllResult.KEY_LINE_NEW_RETURNDATE;
                    } else if (text.contains("Status:")) {
                        nextNodeIs = ProlongAllResult.KEY_LINE_MESSAGE;
                    } else if (text.contains("Mediennummer:") || text.contains("Signatur:")) {
                        nextNodeIs = "";
                    } else if (nextNodeIs.length() > 0) {
                        line.put(nextNodeIs, text.trim());
                        nextNodeIs = "";
                    }
                }
                result.add(line);
            }
            return new ProlongAllResult(MultiStepResult.Status.OK, result);
        }

        return new ProlongAllResult(MultiStepResult.Status.ERROR,
                stringProvider.getString(StringProvider.COULD_NOT_LOAD_ACCOUNT));
    }

    @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 {
        start(); // TODO: Is this necessary?
        boolean success = login(account);
        if (!success) {
            throw new NotReachableException("Login unsuccessful");
        }
    }

    @Override
    public void setLanguage(String language) {
        // TODO Auto-generated method stub

    }

    @Override
    public Set<String> getSupportedLanguages() throws IOException {
        // TODO Auto-generated method stub
        return null;
    }
}