/**
* Copyright (C) 2015 by Simon Legner 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.client.utils.URIBuilder;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
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.networking.HttpClientFactory;
import de.geeksfactory.opacclient.objects.Copy;
import de.geeksfactory.opacclient.objects.Detail;
import de.geeksfactory.opacclient.objects.DetailedItem;
import de.geeksfactory.opacclient.objects.Filter;
import de.geeksfactory.opacclient.objects.Library;
import de.geeksfactory.opacclient.objects.SearchRequestResult;
import de.geeksfactory.opacclient.objects.SearchResult;
import de.geeksfactory.opacclient.searchfields.DropdownSearchField;
import de.geeksfactory.opacclient.searchfields.SearchField;
import de.geeksfactory.opacclient.searchfields.SearchQuery;
import de.geeksfactory.opacclient.searchfields.TextSearchField;
/**
* An implementation of *.web-opac.at sites operated by the Austrian company
* <a href="https://littera.eu/">Littera</a>.
*/
public class Littera extends SearchOnlyApi {
protected static final Map<String, String> LANGUAGE_CODES = new HashMap<String, String>() {{
put("en", "eng");
put("de", "deu");
put("tr", "tur");
}};
protected static final Map<String, SearchResult.MediaType> MEDIA_TYPES =
new HashMap<String, SearchResult.MediaType>() {{
// de
put("Book", SearchResult.MediaType.BOOK);
put("Zeitschrift", SearchResult.MediaType.MAGAZINE);
put("CD ROM", SearchResult.MediaType.CD_SOFTWARE);
put("DVD", SearchResult.MediaType.DVD);
put("Hörbuch", SearchResult.MediaType.AUDIOBOOK);
put("eMedium", SearchResult.MediaType.EBOOK);
// en
put("Book", SearchResult.MediaType.BOOK);
put("Periodical", SearchResult.MediaType.MAGAZINE);
put("CD ROM", SearchResult.MediaType.CD_SOFTWARE);
put("DVD", SearchResult.MediaType.DVD);
put("Audiobook", SearchResult.MediaType.AUDIOBOOK);
put("eMedium", SearchResult.MediaType.EBOOK);
}};
protected static final List<String> SEARCH_FIELDS_FOR_DROPDOWN =
Arrays.asList("ma", "sy", "og");
protected String opac_url = "";
protected String languageCode;
protected List<SearchQuery> lastQuery;
@Override
public void init(Library library, HttpClientFactory httpClientFactory) {
super.init(library, httpClientFactory);
try {
this.opac_url = library.getData().getString("baseurl");
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
protected String getApiUrl() {
return opac_url + "/search?lang=" + getLanguage();
}
@Override
public SearchRequestResult search(List<SearchQuery> query)
throws IOException, OpacErrorException, JSONException {
lastQuery = query;
return executeSearch(query, 1);
}
@Override
public SearchRequestResult searchGetPage(int page)
throws IOException, OpacErrorException, JSONException {
return executeSearch(lastQuery, page);
}
protected SearchRequestResult executeSearch(List<SearchQuery> query, int pageIndex)
throws IOException, OpacErrorException, JSONException {
final String searchUrl;
if (!initialised) {
start();
}
try {
searchUrl = buildSearchUrl(query, pageIndex);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
final String html = httpGet(searchUrl, getDefaultEncoding());
final Document doc = Jsoup.parse(html);
final Element navigation = doc.select(".result_view .navigation").first();
final int totalResults = navigation != null ? parseTotalResults(navigation.text()) : 0;
final Element ul = doc.select(".result_view ul.list").first();
final List<SearchResult> results = new ArrayList<>();
for (final Element li : ul.children()) {
if (li.hasClass("zugangsmonat")) {
continue;
}
final SearchResult result = new SearchResult();
final Element title = li.select(".titelinfo a").first();
result.setId(getQueryParamsFirst(title.attr("href")).get("id"));
result.setInnerhtml(title.text() + "<br>" + title.parent().nextElementSibling().text());
result.setNr(results.size());
result.setPage(pageIndex);
result.setType(MEDIA_TYPES.get(li.select(".statusinfo .ma").text()));
result.setCover(getCover(li));
final String statusImg = li.select(".status img").attr("src");
result.setStatus(statusImg.contains("-yes")
? SearchResult.Status.GREEN
: statusImg.contains("-no")
? SearchResult.Status.RED
: null);
results.add(result);
}
return new SearchRequestResult(results, totalResults, pageIndex);
}
static int parseTotalResults(final String navigation) {
final Matcher matcher = Pattern.compile("(von|of) (\\d+)|(\\d+) sonu\\u00e7tan")
.matcher(navigation);
if (matcher.find()) {
final String num1 = matcher.group(2);
return Integer.parseInt(num1 != null ? num1 : matcher.group(3));
} else {
return 0;
}
}
protected String buildSearchUrl(final List<SearchQuery> query, final int page)
throws IOException, JSONException, URISyntaxException {
final URIBuilder builder = new URIBuilder(getApiUrl());
final List<SearchQuery> nonEmptyQuery = new ArrayList<>();
for (SearchQuery q : query) {
if (q.getValue().equals("")) continue;
if (q.getKey().startsWith("sort")) {
builder.addParameter(q.getKey(), q.getValue());
} else {
nonEmptyQuery.add(q);
}
}
if (nonEmptyQuery.isEmpty()) {
builder.setParameter("mode", "n");
} else if (nonEmptyQuery.size() == 1 && "q".equals(nonEmptyQuery.get(0).getSearchField().getId())) {
builder.setParameter("mode", "s");
builder.setParameter(nonEmptyQuery.get(0).getKey(), nonEmptyQuery.get(0).getValue());
} else {
int i = 0;
for (SearchQuery q : nonEmptyQuery) {
// crit_, value_, op_ are 0-indexed
String key = q.getKey();
String value = q.getValue();
if ("q".equals(key)) {
// fall back to title since free search cannot be combined with other criteria
key = "ht";
value = "*" + value + "*";
}
builder.setParameter("crit_" + i, key);
builder.setParameter("value_" + i, value);
if (i > 0) {
builder.setParameter("op_" + i, "AND");
}
i++;
}
builder.setParameter("mode", "a");
builder.setParameter("critCount", String.valueOf(i)); // 1-index
}
builder.setParameter("page", String.valueOf(page)); // 1-indexed
builder.setParameter("page_size", "30");
return builder.build().toString();
}
@Override
public DetailedItem getResultById(String id, String homebranch)
throws IOException, OpacErrorException {
if (!initialised) {
start();
}
final String html = httpGet(getApiUrl() + "&view=detail&id=" + id, getDefaultEncoding());
final Document doc = Jsoup.parse(html);
final Element detailData = doc.select(".detailData").first();
final Element detailTable = detailData.select("table.titel").first();
final Element availabilityTable = doc.select(".bibliothek table").first();
final DetailedItem result = new DetailedItem();
final Copy copy = new Copy();
result.addCopy(copy);
result.setId(id);
result.setCover(getCover(doc));
result.setTitle(detailData.select("h3").first().text());
result.setMediaType(MEDIA_TYPES.get(getCellContent(detailTable, "Medienart|Type of media")));
copy.setStatus(getCellContent(availabilityTable, "Verfügbar|Available"));
copy.setReturnDate(parseCopyReturn(
getCellContent(availabilityTable, "Exemplare verliehen|Copies lent")));
copy.setReservations(getCellContent(availabilityTable, "Reservierungen|Reservations"));
for (final Element tr : detailTable.select("tr")) {
final String desc = tr.child(0).text();
final String content = tr.child(1).text();
if (desc != null && !desc.trim().equals("")) {
result.addDetail(new Detail(desc, content));
} else if (!result.getDetails().isEmpty()) {
final Detail lastDetail = result.getDetails().get(result.getDetails().size() - 1);
lastDetail.setHtml(true);
lastDetail.setContent(lastDetail.getContent() + "\n" + content);
}
}
return result;
}
private String getCellContent(Element detailTable, String pattern) {
final Element first = detailTable.select("td.label:matchesOwn(" + pattern + ")").first();
return first == null ? null : first.nextElementSibling().text();
}
private static String getCover(Element doc) {
return doc.select(".coverimage img").first().attr("src").replaceFirst("&width=\\d+", "");
}
static LocalDate parseCopyReturn(String str) {
if (str == null)
return null;
DateTimeFormatter fmt =
DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN);
final Matcher matcher = Pattern.compile("[0-9.-]{4,}").matcher(str);
if (matcher.find()) {
return fmt.parseLocalDate(matcher.group());
} else {
return null;
}
}
@Override
public SearchRequestResult filterResults(Filter filter, Filter.Option option)
throws IOException, OpacErrorException {
return null;
}
@Override
public DetailedItem getResult(int position) throws IOException, OpacErrorException {
// Not necessary since getResultById returns an ID with every result
return null;
}
@Override
public List<SearchField> parseSearchFields()
throws IOException, OpacErrorException, JSONException {
start();
final List<SearchField> fields = new ArrayList<>();
addSimpleSearchField(fields);
addAdvancedSearchFields(fields);
addSortingSearchFields(fields);
return fields;
}
protected void addSimpleSearchField(List<SearchField> fields)
throws IOException, JSONException {
final String html = httpGet(getApiUrl() + "&mode=s", getDefaultEncoding());
final Document doc = Jsoup.parse(html);
final Element simple = doc.select(".simple_search").first();
final TextSearchField field = new TextSearchField();
field.setFreeSearch(true);
field.setDisplayName(simple.select("h4").first().text());
field.setId(simple.select("#keyboard").first().attr("name"));
field.setHint("");
field.setData(new JSONObject());
field.getData().put("meaning", field.getId());
fields.add(field);
}
protected void addAdvancedSearchFields(List<SearchField> fields)
throws IOException, JSONException {
final String html = httpGet(getApiUrl() + "&mode=a", getDefaultEncoding());
final Document doc = Jsoup.parse(html);
final Elements options = doc.select("select#adv_search_crit_0").first().select("option");
for (final Element option : options) {
final SearchField field;
if (SEARCH_FIELDS_FOR_DROPDOWN.contains(option.val())) {
field = new DropdownSearchField();
addDropdownValuesForField(((DropdownSearchField) field), option.val());
} else {
field = new TextSearchField();
((TextSearchField) field).setHint("");
}
field.setDisplayName(option.text());
field.setId(option.val());
field.setData(new JSONObject());
field.getData().put("meaning", field.getId());
fields.add(field);
}
}
protected void addDropdownValuesForField(DropdownSearchField field, String id)
throws IOException, JSONException {
field.addDropdownValue("", "");
final String url = opac_url + "/search/adv_ac?crit=" + id;
final String json = httpGet(url, getDefaultEncoding());
try {
final JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
final JSONObject obj = array.getJSONObject(i);
field.addDropdownValue(obj.getString("value"), obj.getString("label"));
}
} catch (JSONException e) {
if (json.startsWith("[")) {
throw e;
} else {
// This is probably a different format
final String[] lines = json.split("\n");
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if (!line.equals("")) {
if (line.contains("|")) {
String[] parts = line.split("\\|");
field.addDropdownValue(parts[0], parts[1]);
} else {
field.addDropdownValue(line, line);
}
}
}
}
}
}
protected void addSortingSearchFields(List<SearchField> fields)
throws IOException, JSONException {
final String html = httpGet(getApiUrl() + "&mode=a", getDefaultEncoding());
final Document doc = Jsoup.parse(html);
for (int i = 0; i < 3; i++) {
final Element tr = doc.select("#sort_editor tr.sort_" + i).first();
final DropdownSearchField field = new DropdownSearchField();
field.setMeaning(SearchField.Meaning.ORDER);
field.setId("sort_" + i);
field.setDisplayName(tr.select("td").first().text());
field.addDropdownValue("", "");
for (final Element option : tr.select(".crit option")) {
if (option.hasAttr("selected")) {
field.addDropdownValue(0, option.attr("value"), option.text());
} else {
field.addDropdownValue(option.attr("value"), option.text());
}
}
fields.add(field);
}
}
@Override
protected String getDefaultEncoding() {
return "utf-8";
}
@Override
public String getShareUrl(String id, String title) {
return getApiUrl() + "&view=detail&id=" + id;
}
@Override
public int getSupportFlags() {
return SUPPORT_FLAG_ENDLESS_SCROLLING;
}
@Override
public Set<String> getSupportedLanguages() throws IOException {
final String html = httpGet(getApiUrl() + "&mode=a", getDefaultEncoding());
final Document doc = Jsoup.parse(html);
final String menuHtml = doc.select(".mainmenu").first().html();
final Set<String> languages = new HashSet<>();
for (final Map.Entry<String, String> i : LANGUAGE_CODES.entrySet()) {
if (menuHtml.contains("lang=" + i.getValue()) /* language switch link */
|| menuHtml.contains("/" + i.getValue() + "/") /* help link */) {
languages.add(i.getKey());
}
}
return languages;
}
@Override
public void setLanguage(String language) {
if (initialised && supportedLanguages.contains(language)) {
this.languageCode = LANGUAGE_CODES.get(language);
}
}
protected String getLanguage() {
return languageCode != null ? languageCode : "eng";
}
}