Fully documented all Kotlin files and added TODOs to functions that need to be updated in some way

This commit is contained in:
denizk0461
2023-04-23 11:57:03 +02:00
parent 2534849329
commit fbad84aa09
33 changed files with 1220 additions and 571 deletions
@@ -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 &amp;, 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("&amp;", "&")
.replace("&gt;", ">")
.replace("&lt;", "<")
/*
* 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"