package de.geeksfactory.opacclient.apis;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.message.BasicNameValuePair;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.json.JSONArray;
import org.json.JSONException;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.FormElement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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.DetailedItem;
import de.geeksfactory.opacclient.objects.LentItem;
import de.geeksfactory.opacclient.objects.Library;
import de.geeksfactory.opacclient.objects.ReservedItem;
/**
* API for the PICA OPAC by OCLC combined with LBS account functions Tested with LBS 4 in TU
* Hamburg-Harburg
*
* @author Johan von Forstner, 30.08.2015
*/
public class PicaLBS extends Pica {
private String lbsUrl;
public void init(Library lib, HttpClientFactory httpClientFactory) {
super.init(lib, httpClientFactory);
this.lbsUrl = data.optString("lbs_url", this.opac_url);
}
@Override
public ReservationResult reservation(DetailedItem item, Account account,
int useraction, String selection) throws IOException {
try {
JSONArray json = new JSONArray(item.getReservation_info());
if (json.length() != 1) {
// TODO: This case is not implemented, don't know if it is possible with LBS
ReservationResult res = new ReservationResult(MultiStepResult.Status.ERROR);
res.setMessage(stringProvider.getString(StringProvider.INTERNAL_ERROR));
return res;
} else {
String url = json.getJSONObject(0).getString("link");
Document doc = Jsoup.parse(httpGet(url, getDefaultLBSEncoding()));
if (doc.select("#opacVolumesForm").size() == 0) {
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("j_username", account.getName()));
params.add(new BasicNameValuePair("j_password", account.getPassword()));
params.add(new BasicNameValuePair("login", "Login"));
doc = Jsoup.parse(httpPost(url,
new UrlEncodedFormEntity(params), getDefaultLBSEncoding()));
}
if (doc.select(".error, font[color=red]").size() > 0) {
ReservationResult res = new ReservationResult(MultiStepResult.Status.ERROR);
res.setMessage(doc.select(".error, font[color=red]").text());
return res;
}
System.out.println(doc.text());
List<Connection.KeyVal> keyVals =
((FormElement) doc.select("#opacVolumesForm").first()).formData();
List<NameValuePair> params = new ArrayList<>();
for (Connection.KeyVal kv : keyVals) {
params.add(new BasicNameValuePair(kv.key(), kv.value()));
}
doc = Jsoup.parse(
httpPost(url, new UrlEncodedFormEntity(params), getDefaultEncoding()));
if (doc.select(".error").size() > 0) {
ReservationResult res = new ReservationResult(MultiStepResult.Status.ERROR);
res.setMessage(doc.select(".error").text());
return res;
} else if (doc.select(".info").text().contains("Reservation saved")
|| doc.select(".info").text().contains("vorgemerkt")) {
return new ReservationResult(MultiStepResult.Status.OK);
} else {
ReservationResult res = new ReservationResult(MultiStepResult.Status.ERROR);
res.setMessage(stringProvider.getString(StringProvider.UNKNOWN_ERROR));
return res;
}
}
} catch (JSONException e) {
e.printStackTrace();
ReservationResult res = new ReservationResult(MultiStepResult.Status.ERROR);
res.setMessage(stringProvider.getString(StringProvider.INTERNAL_ERROR));
return res;
}
}
@Override
public ProlongResult prolong(String media, Account account, int useraction,
String selection) throws IOException {
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("renew", "Renew"));
params.add(new BasicNameValuePair("_volumeNumbersToRenew", ""));
params.add(new BasicNameValuePair("volumeNumbersToRenew", media));
String html = httpPost(lbsUrl + "/LBS_WEB/borrower/loans.htm",
new UrlEncodedFormEntity(params), getDefaultLBSEncoding());
Document doc = Jsoup.parse(html);
String message = doc.select(".alertmessage").text();
if (message.contains("wurde verlängert") || message.contains("has been renewed")) {
return new ProlongResult(MultiStepResult.Status.OK);
} else {
return new ProlongResult(MultiStepResult.Status.ERROR, message);
}
}
@Override
public ProlongAllResult prolongAll(Account account, int useraction, String selection)
throws IOException {
return null;
}
@Override
public CancelResult cancel(String media, Account account, int useraction,
String selection) throws IOException, OpacErrorException {
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("cancel", "Cancel reservation"));
params.add(new BasicNameValuePair("_volumeReservationsToCancel", ""));
params.add(new BasicNameValuePair("volumeReservationsToCancel", media));
String html = httpPost(lbsUrl + "/LBS_WEB/borrower/reservations.htm",
new UrlEncodedFormEntity(params), getDefaultLBSEncoding());
Document doc = Jsoup.parse(html);
String message = doc.select(".alertmessage").text();
if (message.contains("ist storniert") || message.contains("has been cancelled")) {
return new CancelResult(MultiStepResult.Status.OK);
} else {
return new CancelResult(MultiStepResult.Status.ERROR, message);
}
}
@Override
public AccountData account(Account account)
throws IOException, JSONException, OpacErrorException {
if (!initialised) {
start();
}
login(account);
AccountData adata = new AccountData(account.getId());
Document dataDoc = Jsoup.parse(
httpGet(lbsUrl + "/LBS_WEB/borrower/borrower.htm", getDefaultLBSEncoding()));
adata.setPendingFees(extractAccountInfo(dataDoc, "Total Costs", "Gesamtbetrag Kosten"));
adata.setValidUntil(extractAccountInfo(dataDoc, "Expires at", "endet am"));
Document lentDoc = Jsoup.parse(
httpGet(lbsUrl + "/LBS_WEB/borrower/loans.htm", getDefaultLBSEncoding()));
adata.setLent(parseMediaList(lentDoc, stringProvider));
Document reservationsDoc = Jsoup.parse(
httpGet(lbsUrl + "/LBS_WEB/borrower/reservations.htm", getDefaultLBSEncoding()));
adata.setReservations(parseResList(reservationsDoc, stringProvider));
return adata;
}
static List<LentItem> parseMediaList(Document doc, StringProvider stringProvider) {
List<LentItem> lent = new ArrayList<>();
for (Element tr : doc.select(".resultset > tbody > tr:has(.rec_title)")) {
LentItem item = new LentItem();
if (tr.select("input[name=volumeNumbersToRenew]").size() > 0) {
item.setProlongData(tr.select("input[name=volumeNumbersToRenew]").val());
} else {
item.setRenewable(false);
}
String[] titleAndAuthor = extractTitleAndAuthor(tr);
item.setTitle(titleAndAuthor[0]);
if (titleAndAuthor[1] != null) item.setAuthor(titleAndAuthor[1]);
String returndate =
extractAccountInfo(tr, "Returndate", "ausgeliehen bis", "Ausleihfrist");
item.setDeadline(parseDate(returndate));
StringBuilder status = new StringBuilder();
String statusData = extractAccountInfo(tr, "Status", "Derzeit");
if (statusData != null) status.append(statusData);
String prolong = extractAccountInfo(tr, "No of Renewals", "Anzahl Verlängerungen",
"Verlängerungen");
if (prolong != null && !prolong.equals("0")) {
if (status.length() > 0) status.append(", ");
status.append(prolong).append("x ").append(stringProvider
.getString(StringProvider.PROLONGED_ABBR));
}
String reminder = extractAccountInfo(tr, "Remind.", "Mahnungen");
if (reminder != null && !reminder.equals("0")) {
if (status.length() > 0) status.append(", ");
status.append(reminder).append(" ").append(stringProvider
.getString(StringProvider.REMINDERS));
}
String error = tr.select(".error").text();
if (!error.equals("")) {
if (status.length() > 0) status.append(", ");
status.append(error);
}
item.setStatus(status.toString());
item.setHomeBranch(extractAccountInfo(tr, "Counter", "Theke"));
item.setBarcode(extractAccountInfo(tr, "Shelf mark", "Signatur"));
lent.add(item);
}
return lent;
}
private static String[] extractTitleAndAuthor(Element tr) {
String[] titleAndAuthor = new String[2];
String titleAuthor;
if (tr.select(".titleLine").size() > 0) {
titleAuthor = tr.select(".titleLine").text();
} else {
titleAuthor = extractAccountInfo(tr, "Title / Author", "Titel");
}
if (titleAuthor != null) {
String[] parts = titleAuthor.split(" / ");
titleAndAuthor[0] = parts[0];
if (parts.length == 2) {
if (parts[1].endsWith(":")) {
parts[1] = parts[1].substring(0, parts[1].length() - 1).trim();
}
titleAndAuthor[1] = parts[1];
}
}
return titleAndAuthor;
}
private static LocalDate parseDate(String date) {
try {
if (date.matches("\\d\\d.\\d\\d.\\d\\d\\d\\d")) {
return DateTimeFormat.forPattern("dd.MM.yyyy").withLocale(Locale.GERMAN)
.parseLocalDate(date);
} else if (date.matches("\\d\\d/\\d\\d/\\d\\d\\d\\d")) {
return DateTimeFormat.forPattern("dd/MM/yyyy").withLocale(Locale.ENGLISH)
.parseLocalDate(date);
} else {
return null;
}
} catch (IllegalArgumentException e) {
return null;
}
}
static List<ReservedItem> parseResList(Document doc, StringProvider stringProvider) {
List<ReservedItem> reservations = new ArrayList<>();
for (Element tr : doc.select(".resultset > tbody > tr:has(.rec_title)")) {
ReservedItem item = new ReservedItem();
if (tr.select("input[name=volumeReservationsToCancel]").size() > 0) {
item.setCancelData(tr.select("input[name=volumeReservationsToCancel]").val());
}
String[] titleAndAuthor = extractTitleAndAuthor(tr);
item.setTitle(titleAndAuthor[0]);
if (titleAndAuthor[1] != null) item.setAuthor(titleAndAuthor[1]);
item.setBranch(extractAccountInfo(tr, "Destination", "Theke"));
// not supported: extractAccountInfo(tr, "Shelf mark", "Signatur")
StringBuilder status = new StringBuilder();
String numberOfReservations =
extractAccountInfo(tr, "Vormerkung", "Number of reservations");
if (numberOfReservations != null) {
try {
status.append(stringProvider.getQuantityString(
StringProvider.RESERVATIONS_NUMBER,
Integer.parseInt(numberOfReservations.trim()),
Integer.parseInt(numberOfReservations.trim())));
} catch (NumberFormatException e) {
status.append(numberOfReservations);
}
}
String reservationDate = extractAccountInfo(tr, "Reservationdate", "Vormerkungsdatum");
if (reservationDate != null) {
if (status.length() > 0) {
status.append(", ");
}
status.append(stringProvider.getFormattedString(
StringProvider.RESERVED_AT_DATE, reservationDate));
}
if (status.length() > 0) item.setStatus(status.toString());
// TODO: I don't know how reservations are marked that are already available
reservations.add(item);
}
return reservations;
}
private static String extractAccountInfo(Element doc, String... dataNames) {
StringBuilder labelSelector = new StringBuilder();
boolean first = true;
for (String dataName : dataNames) {
if (first) {
first = false;
} else {
labelSelector.append(", ");
}
labelSelector.append(".rec_data > .label:contains(").append(dataName).append(")");
}
if (doc.select(labelSelector.toString()).size() > 0) {
String data = doc.select(labelSelector.toString()).first()
.parent() // td
.parent() // tr
.select("td").get(1) // second column
.text();
if (data.equals("")) return null; else return data;
} else {
return null;
}
}
@Override
public void checkAccountData(Account account)
throws IOException, JSONException, OpacErrorException {
login(account);
}
private void login(Account account) throws IOException, OpacErrorException {
// check if already logged in
String html = httpGet(lbsUrl + "/LBS_WEB/borrower/borrower.htm",
getDefaultLBSEncoding(), true);
if (!html.contains("Login") && !html.equals("")) return;
// Get JSESSIONID cookie
httpGet(lbsUrl + "/LBS_WEB/borrower/borrower.htm?USR=1000&BES=1&LAN=" + getLang(),
getDefaultLBSEncoding());
List<NameValuePair> data = new ArrayList<>();
data.add(new BasicNameValuePair("j_username", account.getName()));
data.add(new BasicNameValuePair("j_password", account.getPassword()));
Document doc = Jsoup.parse(httpPost(lbsUrl + "/LBS_WEB/j_spring_security_check",
new UrlEncodedFormEntity(data), getDefaultLBSEncoding()));
if (doc.select("font[color=red]").size() > 0) {
throw new OpacErrorException(doc.select("font[color=red]").text());
}
}
private String getDefaultLBSEncoding() {
return "ISO-8859-1";
}
}