Archived
Fully documented all Kotlin files and added TODOs to functions that need to be updated in some way
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
package com.denizk0461.studip.data
|
||||
|
||||
import com.denizk0461.studip.db.EventRepository
|
||||
import com.denizk0461.studip.db.AppRepository
|
||||
|
||||
/**
|
||||
* Static object that holds a reference to the app's repository to provide abstracted access to the
|
||||
* app database.
|
||||
*/
|
||||
object Dependencies {
|
||||
lateinit var repo: EventRepository
|
||||
/**
|
||||
* Reference to the app's repository. Since it instantiated once by the main activity, it can be
|
||||
* accessed statically by all other classes. Used for database transactions.
|
||||
*/
|
||||
lateinit var repo: AppRepository
|
||||
}
|
||||
@@ -1,41 +1,26 @@
|
||||
package com.denizk0461.studip.data
|
||||
|
||||
import com.denizk0461.studip.R
|
||||
|
||||
/**
|
||||
* Miscellaneous functions and variables that don't belong into any specific class.
|
||||
*/
|
||||
object Misc {
|
||||
|
||||
private val timeslotAcademicQuarter = mapOf(
|
||||
"6:00" to "6:15",
|
||||
"8:00" to "7:45",
|
||||
"8:00" to "8:15",
|
||||
"10:00" to "9:45",
|
||||
"10:00" to "10:15",
|
||||
"12:00" to "11:45",
|
||||
"12:00" to "12:15",
|
||||
"14:00" to "13:45",
|
||||
"14:00" to "14:15",
|
||||
"16:00" to "15:45",
|
||||
"16:00" to "16:15",
|
||||
"18:00" to "17:45",
|
||||
"18:00" to "18:15",
|
||||
"20:00" to "19:45",
|
||||
"20:00" to "20:15",
|
||||
"22:00" to "21:45",
|
||||
)
|
||||
|
||||
val indexToDrawable: Map<Int, Int> = mapOf(
|
||||
0 to R.drawable.handshake,
|
||||
1 to R.drawable.fish,
|
||||
2 to R.drawable.chicken,
|
||||
3 to R.drawable.sheep,
|
||||
4 to R.drawable.yoga,
|
||||
5 to R.drawable.cow,
|
||||
6 to R.drawable.pig,
|
||||
7 to R.drawable.leaf,
|
||||
8 to R.drawable.carrot,
|
||||
9 to R.drawable.deer,
|
||||
10 to R.drawable.circle,
|
||||
)
|
||||
|
||||
const val mysteryLink = "https://www.youtube.com/watch?v=nhIQMCXJzLI"
|
||||
// private val timeslotAcademicQuarter = mapOf(
|
||||
// "6:00" to "6:15",
|
||||
// "8:00" to "7:45",
|
||||
// "8:00" to "8:15",
|
||||
// "10:00" to "9:45",
|
||||
// "10:00" to "10:15",
|
||||
// "12:00" to "11:45",
|
||||
// "12:00" to "12:15",
|
||||
// "14:00" to "13:45",
|
||||
// "14:00" to "14:15",
|
||||
// "16:00" to "15:45",
|
||||
// "16:00" to "16:15",
|
||||
// "18:00" to "17:45",
|
||||
// "18:00" to "18:15",
|
||||
// "20:00" to "19:45",
|
||||
// "20:00" to "20:15",
|
||||
// "22:00" to "21:45",
|
||||
// )
|
||||
}
|
||||
@@ -3,43 +3,99 @@ package com.denizk0461.studip.data
|
||||
import com.denizk0461.studip.model.StudIPEvent
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
/**
|
||||
* Parser class used for fetching and collecting scheduled events from a Stud.IP timetable.
|
||||
*/
|
||||
class StudIPParser {
|
||||
|
||||
/**
|
||||
* Parse a given HTML string. HTML must be of a Stud.IP timetable.
|
||||
*
|
||||
* @param html website content of the Stud.IP timetable
|
||||
* @param insert action call to save the contents to persistent storage
|
||||
*/
|
||||
fun parse(html: String, insert: (events: List<StudIPEvent>) -> Unit) {
|
||||
// Primary key value to uniquely identify entries in the database
|
||||
var id = 0
|
||||
|
||||
// Parse the HTML using Jsoup to traverse the document
|
||||
val doc = Jsoup.parse(html)
|
||||
|
||||
/*
|
||||
* Create the list that all events will be added to temporarily before saving them to
|
||||
* persistent storage.
|
||||
*/
|
||||
val newEvents = mutableListOf<StudIPEvent>()
|
||||
val columns = arrayOf( // TODO THIS NEEDS TO BE DYNAMIC!
|
||||
|
||||
/*
|
||||
* Fetch all columns from Monday through Friday to parse and assign the events of each day
|
||||
* individually and accordingly.
|
||||
* TODO this must be dynamic. As it is, it assumes that the first day is always Monday,
|
||||
* which needs not be the case. The timetable can be customised by the user by removing
|
||||
* days. Also, Sunday is treated as the first day in Stud.IP FOR SOME REASON, so the app
|
||||
* needs to handle this appropriately.
|
||||
*/
|
||||
val columns = arrayOf(
|
||||
doc.getElementById("calendar_view_1_column_0"),
|
||||
doc.getElementById("calendar_view_1_column_1"),
|
||||
doc.getElementById("calendar_view_1_column_2"),
|
||||
doc.getElementById("calendar_view_1_column_3"),
|
||||
doc.getElementById("calendar_view_1_column_4"),
|
||||
)
|
||||
|
||||
// Iterate through each day
|
||||
columns.forEachIndexed { index, element ->
|
||||
element?.getElementsByClass("schedule_entry")?.forEachIndexed { entryIndex, entry ->
|
||||
|
||||
// Iterate through all events scheduled for a given day
|
||||
element?.getElementsByClass("schedule_entry")?.forEach { entry ->
|
||||
/*
|
||||
* Retrieve the event's header. This will contain the event's title as well as the
|
||||
* lecturers holding the event.
|
||||
*/
|
||||
val entryHeader = entry.getElementsByTag("dt")[0].text()
|
||||
|
||||
/*
|
||||
* Find out the index before which the title ends and after which the lecturer
|
||||
* name(s) begin.
|
||||
*/
|
||||
val delimiter = entryHeader.lastIndexOf('(')
|
||||
val parsedTitle = entryHeader.substring(0 until delimiter)
|
||||
val parsedLecturers = entryHeader.substring(delimiter + 1 until entryHeader.length - 1)
|
||||
|
||||
val entryInfos = entry.getElementsByTag("dd")[0].text().split(", ", limit = 2)
|
||||
// val timeSlot = Misc.parseTimeslot(entryInfos[0])
|
||||
val timeSlot = entryInfos[0].split(" - ")
|
||||
// Retrieve the title of the event from the header string
|
||||
val parsedTitle = entryHeader.substring(0 until delimiter).trim()
|
||||
|
||||
// Retrieve the lecturer name(s) from the header string
|
||||
val parsedLecturers = entryHeader.substring(delimiter + 1 until entryHeader.length - 1).trim()
|
||||
|
||||
/*
|
||||
* Retrieve further event information. This will contain both the time slot the
|
||||
* event is assigned to (index 0), as well as the room the event will primarily take
|
||||
* place in (index 1).
|
||||
*/
|
||||
val entryInfo = entry.getElementsByTag("dd")[0].text().split(", ", limit = 2)
|
||||
|
||||
// Retrieve the time slot and split it into start and end time stamps
|
||||
val timeSlot = entryInfo[0].split(" - ")
|
||||
|
||||
// Construct the newly scraped Stud.IP event
|
||||
val event = StudIPEvent(
|
||||
id = id,
|
||||
title = parsedTitle,
|
||||
lecturer = parsedLecturers,
|
||||
room = entryInfos[1],
|
||||
day = index,
|
||||
timeslotStart = timeSlot[0],
|
||||
timeslotEnd = timeSlot[1],
|
||||
id = id,
|
||||
title = parsedTitle,
|
||||
lecturer = parsedLecturers,
|
||||
room = entryInfo[1],
|
||||
day = index,
|
||||
timeslotStart = timeSlot[0],
|
||||
timeslotEnd = timeSlot[1],
|
||||
)
|
||||
|
||||
// Add the new element to the temporary list
|
||||
newEvents.add(event)
|
||||
|
||||
// Raise the primary key value by 1 to avoid not fulfilling the unique constraint
|
||||
id += 1
|
||||
}
|
||||
}
|
||||
|
||||
// After fetching has finished, save the list of new items into persistent storage
|
||||
insert(newEvents)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,50 @@
|
||||
package com.denizk0461.studip.data
|
||||
|
||||
import android.util.Log
|
||||
import com.denizk0461.studip.db.EventRepository
|
||||
import com.denizk0461.studip.db.AppRepository
|
||||
import com.denizk0461.studip.model.*
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
|
||||
// Parser class for the Studierendenwerk cafeteria plans
|
||||
/**
|
||||
* Parser class used for fetching and collecting canteen offers from the website of the
|
||||
* Studierendenwerk Bremen.
|
||||
*/
|
||||
class StwParser {
|
||||
|
||||
// Unique primary key value for the date elements
|
||||
private var dateId = 0
|
||||
|
||||
// Unique primary key value for the canteen elements
|
||||
private var canteenId = 0
|
||||
|
||||
// Unique primary key value for the category elements
|
||||
private var categoryId = 0
|
||||
|
||||
// Unique primary key value for the item elements
|
||||
private var itemId = 0
|
||||
|
||||
/**
|
||||
* Parses through a list of canteen plans and saves them to persistent storage.
|
||||
*
|
||||
* @param onRefreshUpdate action call for when an update on the status of the fetch is
|
||||
* available
|
||||
* @param onFinish action call for when the fetch has finished
|
||||
*/
|
||||
fun parse(onRefreshUpdate: (status: Int) -> Unit, onFinish: () -> Unit) {
|
||||
/*
|
||||
* Reset primary key values. This needs to be done in case this method is called multiple
|
||||
* times within the app's lifespan
|
||||
*/
|
||||
dateId = 0
|
||||
canteenId = 0
|
||||
categoryId = 0
|
||||
itemId = 0
|
||||
|
||||
/*
|
||||
* Create and populate a list of all the canteen plans that will be fetched.
|
||||
* TODO implement functionality that will allow for fetching multiple plans, if necessary
|
||||
*/
|
||||
val links = mutableListOf<String>()
|
||||
if (true) { links.add(urlUniMensa) }
|
||||
// if (true) { links.add(urlCafeCentral) }
|
||||
@@ -32,66 +57,106 @@ class StwParser {
|
||||
// if (true) { links.add(urlMensaBHV) }
|
||||
// if (true) { links.add(urlCafeBHV) }
|
||||
|
||||
// Delete all previous entries and start afresh
|
||||
Dependencies.repo.nukeOffers()
|
||||
|
||||
// STEP: fetch every canteen
|
||||
/*
|
||||
* Fetch offers from every canteen individually. This is done in sequence for
|
||||
* simplicity's sake.
|
||||
* TODO perhaps it can be assumed here that the next two weeks will always be available?
|
||||
* Hence, the date fetch might be superfluous and a database insert call could be
|
||||
* implemented here instead. Another option would be to set the conflict policy to IGNORE
|
||||
* and move the dateId reset from parseFromPage() to parse().
|
||||
*/
|
||||
links.forEach { link ->
|
||||
// TODO implement onRefresh(Int)
|
||||
parseFromPage(link, Dependencies.repo)
|
||||
|
||||
// TODO implement onRefresh(Int)
|
||||
}
|
||||
|
||||
// Action call once all fetching activities have finished
|
||||
onFinish()
|
||||
}
|
||||
|
||||
private fun parseFromPage(url: String, repo: EventRepository): List<CanteenOffer> {
|
||||
val items = mutableListOf<CanteenOffer>()
|
||||
/**
|
||||
* Fetch and parse the plan of a specific canteen.
|
||||
*
|
||||
* @param url link to the canteen to be scraped. Must be a subpage of stw-bremen.de
|
||||
* @param repo reference to the app repository used to save the items to persistent storage
|
||||
*/
|
||||
private fun parseFromPage(url: String, repo: AppRepository) {
|
||||
// Used to store a parsed date string without creating a new variable on every loop
|
||||
var date: String
|
||||
|
||||
// Reset the primary key value for the date objects
|
||||
dateId = 0
|
||||
|
||||
// Parse the HTML using Jsoup to traverse the document
|
||||
val doc = Jsoup.connect(url).get()
|
||||
|
||||
// Save the canteen to persistent storage
|
||||
repo.insert(OfferCanteen(canteenId, doc.getElementsByClass("pane-title")[1].text()))
|
||||
|
||||
// STEP: get each day
|
||||
// Iterate through each day for which offers are available
|
||||
doc.getElementsByClass("food-plan").forEach { dayPlan ->
|
||||
|
||||
/*
|
||||
* Fetch the date for a given day. It will be in the following format:
|
||||
* 24. Apr
|
||||
* This value will be split between the day (24.) and the month (Apr)
|
||||
*/
|
||||
val rawDate = doc.getElementsByClass("tabs")[0]
|
||||
.getElementsByClass("tab-date")[dateId].text().split(" ")
|
||||
|
||||
/*
|
||||
* Parse the two elements into a single date. The month will be converted to a numeric
|
||||
* value. Example:
|
||||
* Input: 24. Apr
|
||||
* Output: 24.04.
|
||||
*/
|
||||
date = "${rawDate[0]}${rawDate[1].monthToNumber()}."
|
||||
|
||||
val d = OfferDate(dateId, date)
|
||||
Log.d("eek!8", d.toString())
|
||||
repo.insert(d)
|
||||
/*
|
||||
* Save the date to persistent storage.
|
||||
* TODO this will duplicate date entries for every canteen that is fetched. This is
|
||||
* inefficient.
|
||||
*/
|
||||
repo.insert(OfferDate(dateId, date))
|
||||
|
||||
// STEP: get each category in a day
|
||||
// Iterate through all categories offered on a given day
|
||||
dayPlan.getElementsByClass("food-category").forEach { category ->
|
||||
// Retrieve the category text
|
||||
val categoryTitle = category.getElementsByClass("category-name")[0].text()
|
||||
|
||||
// Save the category to persistent storage
|
||||
repo.insert(OfferCategory(categoryId, dateId, canteenId, categoryTitle))
|
||||
|
||||
// STEP: get each item in a category
|
||||
// Iterate through all items in a category
|
||||
category
|
||||
.getElementsByTag("tbody")[0]
|
||||
.getElementsByTag("tr").forEach { element ->
|
||||
// Retrieve the parent element holding the item's title and price
|
||||
val tableRows = element.getElementsByTag("td")
|
||||
|
||||
val prefs =
|
||||
element.getElementsByClass("field field-name-field-food-types")[0]
|
||||
// Retrieve the dietary preferences the item meets
|
||||
val prefs = element
|
||||
.getElementsByClass("field field-name-field-food-types")[0]
|
||||
|
||||
// Parse the preferences into a string for the database
|
||||
val prefString = DietaryPrefObject(
|
||||
isFair = prefs.isDietaryPreferenceMet(PREFERENCE_FAIR),
|
||||
isFish = prefs.isDietaryPreferenceMet(PREFERENCE_FISH),
|
||||
isPoultry = prefs.isDietaryPreferenceMet(PREFERENCE_POULTRY),
|
||||
isLamb = prefs.isDietaryPreferenceMet(PREFERENCE_LAMB),
|
||||
isVital = prefs.isDietaryPreferenceMet(PREFERENCE_VITAL),
|
||||
isBeef = prefs.isDietaryPreferenceMet(PREFERENCE_BEEF),
|
||||
isPork = prefs.isDietaryPreferenceMet(PREFERENCE_PORK),
|
||||
isVegan = prefs.isDietaryPreferenceMet(PREFERENCE_VEGAN),
|
||||
isVegetarian = prefs.isDietaryPreferenceMet(PREFERENCE_VEGETARIAN),
|
||||
isGame = prefs.isDietaryPreferenceMet(PREFERENCE_GAME),
|
||||
isFair = prefs.isDietaryPreferenceMet(imageLinkPrefFair),
|
||||
isFish = prefs.isDietaryPreferenceMet(imageLinkPrefFish),
|
||||
isPoultry = prefs.isDietaryPreferenceMet(imageLinkPrefPoultry),
|
||||
isLamb = prefs.isDietaryPreferenceMet(imageLinkPrefLamb),
|
||||
isVital = prefs.isDietaryPreferenceMet(imageLinkPrefVital),
|
||||
isBeef = prefs.isDietaryPreferenceMet(imageLinkPrefBeef),
|
||||
isPork = prefs.isDietaryPreferenceMet(imageLinkPrefPork),
|
||||
isVegan = prefs.isDietaryPreferenceMet(imageLinkPrefVegan),
|
||||
isVegetarian = prefs.isDietaryPreferenceMet(imageLinkPrefVegetarian),
|
||||
isGame = prefs.isDietaryPreferenceMet(imageLinkPrefGame),
|
||||
).deconstruct()
|
||||
|
||||
// Save the item to persistent storage
|
||||
repo.insert(
|
||||
OfferItem(
|
||||
itemId,
|
||||
@@ -101,39 +166,77 @@ class StwParser {
|
||||
prefString,
|
||||
)
|
||||
)
|
||||
|
||||
// Increment the item ID to avoid overriding
|
||||
itemId += 1
|
||||
}
|
||||
// Increment the category ID to avoid overriding
|
||||
categoryId += 1
|
||||
}
|
||||
// Increment the date ID to avoid overriding
|
||||
dateId += 1
|
||||
}
|
||||
// Increment the canteen ID to avoid overriding
|
||||
canteenId += 1
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates whether a dietary preference is met by checking if the link to an image can be
|
||||
* found in the HTML.
|
||||
*
|
||||
* @param preference constraint that needs to be met
|
||||
* @return whether it is met
|
||||
*/
|
||||
private fun Element.isDietaryPreferenceMet(preference: String): Boolean =
|
||||
getElementsByAttributeValue("src", preference).isNotEmpty()
|
||||
|
||||
/**
|
||||
* Processes certain character references into human-readable characters. Since the fetched HTML
|
||||
* contains unresolved symbols such as &, they need to be replaced with their counterpart
|
||||
* (in this case, &).
|
||||
*
|
||||
* @return the filtered string
|
||||
*/
|
||||
private fun Element.getFilteredText(): String {
|
||||
// Retrieve the element's inner HTML and replace faulty characters
|
||||
var text = html()
|
||||
.replace("&", "&")
|
||||
.replace(">", ">")
|
||||
.replace("<", "<")
|
||||
|
||||
/*
|
||||
* The Studierendenwerk's website lists allergens, but they are invisible, rendering them
|
||||
* pointless to the website user. As of now, this information is discarded in the app.
|
||||
* TODO implement allergen functionality
|
||||
*/
|
||||
while (text.contains("<sup>")) {
|
||||
text = text.substring(0 until text.indexOf("<sup>")) + text.substring(text.indexOf("</sup>")+6 until text.length)
|
||||
}
|
||||
|
||||
// Return the stripped and updated HTML string
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the text of an element in a certain position if the element can be found. Else,
|
||||
* return an empty string.
|
||||
*
|
||||
* @param index position at which a child
|
||||
* @return text content or, if no element is found, an empty string
|
||||
*/
|
||||
private fun Elements.getTextOrEmpty(index: Int): String = try {
|
||||
get(index).text()
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
""
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a month text string to its numeric value. Example:
|
||||
* Input: Apr
|
||||
* Output: 04
|
||||
*
|
||||
* @return the numeric value of the month
|
||||
*/
|
||||
private fun String.monthToNumber(): String = when (this) {
|
||||
"Jan" -> "01"
|
||||
"Feb" -> "02"
|
||||
@@ -150,22 +253,24 @@ class StwParser {
|
||||
else -> "00" // shouldn't occur
|
||||
}
|
||||
|
||||
private val PREFERENCE_FAIR = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
||||
private val PREFERENCE_FISH = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
||||
private val PREFERENCE_POULTRY = "https://www.stw-bremen.de/sites/default/files/images/pictograms/geflugel.png"
|
||||
private val PREFERENCE_LAMB = "https://www.stw-bremen.de/sites/default/files/images/pictograms/lamm.png"
|
||||
private val PREFERENCE_VITAL = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vital.png"
|
||||
private val PREFERENCE_BEEF = "https://www.stw-bremen.de/sites/default/files/images/pictograms/rindfleisch.png"
|
||||
private val PREFERENCE_PORK = "https://www.stw-bremen.de/sites/default/files/images/pictograms/schwein.png"
|
||||
private val PREFERENCE_VEGAN = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vegan.png"
|
||||
private val PREFERENCE_VEGETARIAN = "https://www.stw-bremen.de/sites/default/files/images/pictograms/vegetarisch.png"
|
||||
private val PREFERENCE_GAME = "https://www.stw-bremen.de/sites/default/files/images/pictograms/wild.png"
|
||||
// Image links to all dietary preferences used for checking whether a preference is met
|
||||
private val imageLinkPrefFair = "https://www.stw-bremen.de/sites/default/files/images/pictograms/at_small.png"
|
||||
private val imageLinkPrefFish = "https://www.stw-bremen.de/sites/default/files/images/pictograms/fisch.png"
|
||||
private val imageLinkPrefPoultry = "https://www.stw-bremen.de/sites/default/files/images/pictograms/geflugel.png"
|
||||
private val imageLinkPrefLamb = "https://www.stw-bremen.de/sites/default/files/images/pictograms/lamm.png"
|
||||
private val imageLinkPrefVital = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vital.png"
|
||||
private val imageLinkPrefBeef = "https://www.stw-bremen.de/sites/default/files/images/pictograms/rindfleisch.png"
|
||||
private val imageLinkPrefPork = "https://www.stw-bremen.de/sites/default/files/images/pictograms/schwein.png"
|
||||
private val imageLinkPrefVegan = "https://www.stw-bremen.de/sites/default/files/images/pictograms/mensa_vegan.png"
|
||||
private val imageLinkPrefVegetarian = "https://www.stw-bremen.de/sites/default/files/images/pictograms/vegetarisch.png"
|
||||
private val imageLinkPrefGame = "https://www.stw-bremen.de/sites/default/files/images/pictograms/wild.png"
|
||||
|
||||
// URLs to all canteens in Bremen and Bremerhaven managed by the Studierendenwerk Bremen
|
||||
private val urlUniMensa = "https://www.stw-bremen.de/de/mensa/uni-mensa"
|
||||
private val urlCafeCentral = "https://www.stw-bremen.de/de/mensa/cafe-central"
|
||||
private val urlNW1 = "https://www.stw-bremen.de/de/mensa/nw-1"
|
||||
private val urlGW2 = "https://www.stw-bremen.de/de/cafeteria/gw2"
|
||||
// private val urlGraz = ""
|
||||
// private val urlGraz = "" // No link since there are only snacks on offer that are not listed online
|
||||
private val urlHSBNeustadt = "https://www.stw-bremen.de/de/mensa/neustadtswall"
|
||||
private val urlHSBWerder = "https://www.stw-bremen.de/de/mensa/werderstra%C3%9Fe"
|
||||
private val urlHSBAirport = "https://www.stw-bremen.de/de/mensa/airport"
|
||||
|
||||
Reference in New Issue
Block a user