package org.voidsink.kussslib.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.model.Calendar;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.voidsink.kussslib.Assessment;
import org.voidsink.kussslib.AssessmentType;
import org.voidsink.kussslib.Course;
import org.voidsink.kussslib.Curricula;
import org.voidsink.kussslib.EventType;
import org.voidsink.kussslib.Exam;
import org.voidsink.kussslib.Grade;
import org.voidsink.kussslib.KusssHandler;
import org.voidsink.kussslib.Term;
public class KusssHandlerImpl implements KusssHandler {
private CookieManager mCookies;
private String sessionId = "";
private IExceptionListener exceptionListener = null;
private static final String URL_MY_LVAS = "https://www.kusss.jku.at/kusss/assignment-results.action";
private static final String URL_GET_TERMS = "https://www.kusss.jku.at/kusss/listmystudentlvas.action";
private static final String URL_GET_ICAL = "https://www.kusss.jku.at/kusss/ical-multi-sz.action";
private static final String URL_MY_GRADES = "https://www.kusss.jku.at/kusss/gradeinfo.action";
private static final String URL_START_PAGE = "https://www.kusss.jku.at/kusss/studentwelcome.action";
private static final String URL_LOGOUT = "https://www.kusss.jku.at/kusss/logout.action";
private static final String URL_LOGIN = "https://www.kusss.jku.at/kusss/login.action";
private static final String URL_GET_NEW_EXAMS = "https://www.kusss.jku.at/kusss/szsearchexam.action";
private static final String URL_GET_EXAMS = "https://www.kusss.jku.at/kusss/szexaminationlist.action";
private static final String URL_SELECT_TERM = "https://www.kusss.jku.at/kusss/select-term.action";
private static final String URL_MY_STUDIES = "https://www.kusss.jku.at/kusss/studentsettings.action";
private static final String SELECT_MY_LVAS = "body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > table > tbody > tr:has(td)";
private static final String SELECT_MY_GRADES = "body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > *";
private static final String SELECT_NOT_LOGGED_IN = "body > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > h4";
// private static final String SELECT_ACTUAL_EXAMS =
// "body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > div.tabcontainer > div.tabcontent > table > tbody > tr > td > form > table > tbody > tr:has(td)";
private static final String SELECT_NEW_EXAMS = "body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > div.tabcontainer > div.tabcontent > div.sidetable > form > table > tbody > tr:has(td)";
private static final String SELECT_EXAMS = "body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > div.tabcontainer > div.tabcontent > table > tbody > tr > td > form > table > tbody > tr:has(td)";
private static final String SELECT_MY_STUDIES = "body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > div.tabcontainer > div.tabcontent > form > table > tbody > tr[class]:has(td)";
private static final int TIMEOUT_LOGIN = 15 * 1000; // 15s
private static final int TIMEOUT_SEARCH_EXAM_BY_LVA = 10 * 1000; // 10s
private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
private static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
private void onHandleException(Exception e, boolean fatal) {
if (exceptionListener != null) {
exceptionListener.onExceptionOccured(e, fatal);
}
}
@Override
public synchronized boolean login(String user, String password) {
if (user == null || password == null) {
return false;
}
try {
if ((user.length() > 0) && (user.charAt(0) != 'k')) {
user = "k" + user;
}
Document doc = Jsoup.connect(URL_LOGIN).timeout(TIMEOUT_LOGIN)
.data("j_username", user).data("j_password", password)
.post();
// TODO: check document for successful login message
sessionId = getSessionIDFromCookie();
if (isLoggedIn()) {
return true;
}
sessionId = null;
return false;
} catch (SocketTimeoutException e) {
// bad connection, timeout
sessionId = null;
return false;
} catch (Exception e) {
onHandleException(e, true);
sessionId = null;
return false;
}
}
private String getSessionIDFromCookie() {
try {
List<HttpCookie> cookies = mCookies.getCookieStore().get(
new URI("https://www.kusss.jku.at/"));
for (HttpCookie cookie : cookies) {
if (cookie.getName().equals("JSESSIONID")) {
return cookie.getValue();
}
}
return null;
} catch (URISyntaxException e) {
return null;
}
}
@Override
public boolean logout() {
try {
Connection.Response r = Jsoup.connect(URL_LOGOUT)
.method(Connection.Method.GET).execute();
if (r == null) {
return false;
}
return !isLoggedIn();
} catch (Exception e) {
onHandleException(e, true);
return true;
}
}
@Override
public boolean isLoggedIn() {
try {
String actSessionId = getSessionIDFromCookie();
if (actSessionId == null || sessionId == null
|| !sessionId.equals(actSessionId)) {
return false;
}
Document doc = Jsoup.connect(URL_START_PAGE).timeout(TIMEOUT_LOGIN)
.get();
Elements notLoggedIn = doc.select(SELECT_NOT_LOGGED_IN);
if (notLoggedIn.size() > 0) {
return false;
}
} catch (SocketTimeoutException e) {
// bad connection, timeout
return false;
} catch (IOException e) {
onHandleException(e, true);
return false;
}
return true;
}
@Override
public Calendar getEvents(EventType eventType,
CalendarBuilder calendarBuilder) {
Calendar iCal = null;
try {
URL url = new URL(URL_GET_ICAL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setConnectTimeout(5000);
conn.setReadTimeout(15000);
switch (eventType) {
case EXAM: {
writeParams(conn, new String[] { "selectAll" },
new String[] { "ical.category.examregs" });
break;
}
case COURSE: {
writeParams(conn, new String[] { "selectAll" },
new String[] { "ical.category.mycourses" });
break;
}
}
BufferedInputStream in = new BufferedInputStream(
conn.getInputStream());
iCal = calendarBuilder.build(in);
conn.disconnect();
} catch (Exception e) {
onHandleException(e, true);
iCal = null;
}
return iCal;
}
private void writeParams(URLConnection conn, String[] keys, String[] values)
throws IOException {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < keys.length; i++) {
builder.append(keys[i]);
builder.append("=");
builder.append(values[i]);
if (i < keys.length - 1) {
builder.append("&");
}
}
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
wr.write(builder.toString());
wr.flush();
}
@Override
public Calendar getEvents(EventType eventType) {
CalendarBuilder calendarBuilder = new CalendarBuilder();
return getEvents(eventType, calendarBuilder);
}
@Override
public List<Assessment> getAssessments() {
List<Assessment> assessments = new ArrayList<>();
try {
Document doc = Jsoup.connect(URL_MY_GRADES).data("months", "0")
.get();
Elements rows = doc.select(SELECT_MY_GRADES);
AssessmentType type = null;
for (Element row : rows) {
if (row.tag().toString().equals("h3")) {
type = AssessmentType.parseAssessmentType(row.text());
} else if (row.tag().toString().equals("table")) {
Elements gradeRows = row
.select("tbody > tr[class]:has(td)");
for (Element gradeRow : gradeRows) {
AssessmentImpl assessment = new AssessmentImpl(type,
gradeRow);
if (assessment.isInitialized()) {
assessments.add(assessment);
}
}
}
}
} catch (IOException | ParseException e) {
onHandleException(e, true);
return null;
}
return assessments;
}
@Override
public List<Exam> getExams() {
List<Exam> exams = new ArrayList<>();
try {
Document doc = Jsoup.connect(URL_GET_NEW_EXAMS)
.data("search", "true").data("searchType", "mylvas").get();
Elements rows = doc.select(SELECT_NEW_EXAMS);
int i = 0;
while (i < rows.size()) {
Element row = rows.get(i);
ExamImpl exam = new ExamImpl(row, true);
i++;
if (exam.isInitialized()) {
while (i < rows.size()
&& rows.get(i).attr("class")
.equals(row.attr("class"))) {
exam.addAdditionalInfo(rows.get(i));
i++;
}
exams.add(exam);
}
}
// add registered exams
loadRegisteredExams(exams);
} catch (Exception e) {
onHandleException(e, true);
return null;
}
return exams;
}
@Override
public List<Exam> getExamsByCourses(List<Course> courses) {
if (courses == null || courses.size() == 0) {
return null;
}
List<Exam> exams = new ArrayList<>();
try {
Map<String, Assessment> gradeCache = new HashMap<>();
List<Assessment> assessments = getAssessments();
if (assessments != null) {
for (Assessment assessment : assessments) {
if (!assessment.getCourseId().isEmpty()) {
Assessment existing = gradeCache.get(assessment
.getCourseId());
if (existing != null) {
// Log.d(TAG,
// existing.getTitle() + " --> "
// + assessment.getTitle());
}
gradeCache.put(assessment.getCourseId(), assessment);
}
}
}
for (Course course : courses) {
Assessment assessment = gradeCache.get(course.getCourseId());
if (assessment != null) {
if ((assessment.getGrade() == Grade.G5)
|| (assessment.getDate().getTime() > (System
.currentTimeMillis() - (182 * DAY_IN_MILLIS)))) {
// Log.d(TAG,
// "positive in last 6 Months: "
// + grade.getTitle());
assessment = null;
}
}
if (assessment == null) {
List<Exam> newExams = getExamsByCourseId(course
.getCourseId());
if (newExams != null) {
for (Exam newExam : newExams) {
if (newExam != null) {
exams.add(newExam);
}
}
}
}
}
// add registered exams
loadRegisteredExams(exams);
} catch (Exception e) {
onHandleException(e, true);
return null;
}
return exams;
}
private List<Exam> getExamsByCourseId(String courseId) {
List<Exam> exams = new ArrayList<>();
try {
final SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy");
Document doc = Jsoup
.connect(URL_GET_NEW_EXAMS)
.timeout(TIMEOUT_SEARCH_EXAM_BY_LVA)
.data("search", "true")
.data("searchType", "specific")
.data("searchDateFrom",
df.format(new Date(System.currentTimeMillis())))
.data("searchDateTo",
df.format(new Date(System.currentTimeMillis()
+ YEAR_IN_MILLIS)))
.data("searchLvaNr", courseId).data("searchLvaTitle", "")
.data("searchCourseClass", "").post();
Elements rows = doc.select(SELECT_NEW_EXAMS);
int i = 0;
while (i < rows.size()) {
Element row = rows.get(i);
ExamImpl exam = new ExamImpl(row, true);
i++;
if (exam.isInitialized()) {
while (i < rows.size()
&& rows.get(i).attr("class")
.equals(row.attr("class"))) {
exam.addAdditionalInfo(rows.get(i));
i++;
}
exams.add(exam);
}
}
} catch (IOException | ParseException e) {
onHandleException(e, true);
exams = null;
}
return exams;
}
private void loadRegisteredExams(List<Exam> exams) throws IOException,
ParseException {
Document doc = Jsoup.connect(URL_GET_EXAMS).get();
Elements rows = doc.select(SELECT_EXAMS);
int i = 0;
while (i < rows.size()) {
Element row = rows.get(i);
ExamImpl exam = new ExamImpl(row, false);
i++;
if (exam.isInitialized()) {
while (i < rows.size()
&& rows.get(i).attr("class").equals(row.attr("class"))) {
exam.addAdditionalInfo(rows.get(i));
i++;
}
exams.add(exam);
}
}
}
@Override
public List<Curricula> getCurricula() {
try {
List<Curricula> curricula = new ArrayList<>();
Document doc = Jsoup.connect(URL_MY_STUDIES).get();
Elements rows = doc.select(SELECT_MY_STUDIES);
for (Element row : rows) {
CurriculaImpl c = new CurriculaImpl(row);
if (c.isInitialized()) {
curricula.add(c);
}
}
return curricula;
} catch (Exception e) {
onHandleException(e, true);
return null;
}
}
public KusssHandlerImpl() {
this.mCookies = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(mCookies);
}
@Override
public void setExceptionListener(KusssHandler.IExceptionListener listener) {
this.exceptionListener = listener;
}
@Override
public List<Course> getCourses(List<Term> terms) {
if (terms == null || terms.size() == 0) {
return null;
}
List<Course> courses = new ArrayList<>();
try {
for (Term term : terms) {
term.setLoaded(false); // init loaded flag
if (selectTerm(term)) {
Document doc = Jsoup.connect(URL_MY_LVAS).get();
if (isSelectable(doc, term)) {
if (isSelected(doc, term)) {
// .select("body.intra > table > tbody > tr > td > table > tbody > tr > td.contentcell > div.contentcell > table > tbody > tr");
Elements rows = doc.select(SELECT_MY_LVAS);
for (Element row : rows) {
CourseImpl course = new CourseImpl(term, row);
if (course.isInitialized()) {
courses.add(course);
}
}
term.setLoaded(true);
} else {
throw new IOException(String.format(
"term not selected: %s", term));
}
}
} else {
// break if selection failed
throw new IOException(String.format(
"cannot select term: %s", term));
}
}
if (courses != null && courses.size() == 0) {
// break if no lvas found, a student without courses is a quite
// impossible case
throw new IOException("no lvas found");
}
} catch (Exception e) {
onHandleException(e, true);
return null;
}
return courses;
}
public boolean selectTerm(Term term) throws IOException {
Document doc = Jsoup.connect(URL_SELECT_TERM)
.data("term", term.toString()).data("previousQueryString", "")
.data("reloadAction", "coursecatalogue-start.action").post();
// TODO: check document for successful selection of term
// if (!isSelected(doc, term)) {
// throw new IOException(String.format("selection of term failed: %s",
// term));
// }
return true;
}
private boolean isSelectable(Document doc, Term term) {
try {
Element termSelector = doc.getElementById("term");
if (termSelector == null)
return false;
Elements selectable = termSelector.getElementsByAttributeValue(
"value", term.toString());
if (selectable.size() != 1)
return false;
return true;
} catch (Exception e) {
onHandleException(e, true);
return false;
}
}
private boolean isSelected(Document doc, Term term) {
try {
Elements terms = doc.getElementById("term").getElementsByAttribute(
"selected");
for (Element termEntry : terms) {
if (termEntry.attr("value").equals(term.toString())) {
return true;
}
}
} catch (Exception e) {
onHandleException(e, true);
return false;
}
return false;
}
}