Skip to content

Commit

Permalink
Support caching via "Last-Modified" & "If-Modified-Since" HTTP headers.
Browse files Browse the repository at this point in the history
+ Add "schedule_last_modified" column to "meta" table.
  Increase "meta" database version.
+ Ensure default value is empty string. Otherwise, the app would crash on
  the next update in RealMetaDatabaseRepository/query/getString with a NPE.
+ Docs:
  + https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
  + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
+ Related:
  + FOSDEM/website#235
  + https://git.cccv.de/hub/hub/-/issues/564
  • Loading branch information
johnjohndoe committed Jan 13, 2024
1 parent 344fa1b commit 0d997a5
Show file tree
Hide file tree
Showing 14 changed files with 66 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import info.metadude.android.eventfahrplan.network.models.HttpHeader as HttpHead

fun HttpHeaderAppModel.toHttpHeaderNetworkModel() = HttpHeaderNetworkModel(
eTag = eTag,
lastModified = lastModified,
)

fun HttpHeaderDatabaseModel.toHttpHeaderAppModel() = HttpHeaderAppModel(
eTag = eTag,
lastModified = lastModified,
)

fun HttpHeaderNetworkModel.toHttpHeaderDatabaseModel() = HttpHeaderDatabaseModel(
eTag = eTag,
lastModified = lastModified,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ package nerd.tuxmobil.fahrplan.congress.models
*/
data class HttpHeader(
val eTag: String = "",
val lastModified: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ class FetchScheduleResultExtensionsTest {
val networkFetchScheduleResult = NetworkFetchScheduleResult(
httpStatus = NetworkHttpStatus.HTTP_NOT_MODIFIED,
scheduleXml = "<xml></xml>",
httpHeader = HttpHeader("mno456"),
httpHeader = HttpHeader(
eTag = "mno456",
lastModified = "2023-12-31T23:59:59+01:00",
),
hostName = "example.com",
exceptionMessage = "SSLException"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import nerd.tuxmobil.fahrplan.congress.models.Meta as MetaAppModel
class MetaExtensionsTest {

private val metaAppModel = MetaAppModel(
httpHeader = HttpHeaderAppModel("abc123"),
httpHeader = HttpHeaderAppModel(
eTag = "abc123",
lastModified = "2019-12-31T23:59:59+01:00",
),
numDays = 23,
subtitle = "My subtitle",
timeZoneId = ZoneId.of("Europe/Berlin"),
Expand All @@ -22,7 +25,10 @@ class MetaExtensionsTest {
)

private val metaDatabaseModel = MetaDatabaseModel(
httpHeader = HttpHeaderDatabaseModel("abc123"),
httpHeader = HttpHeaderDatabaseModel(
eTag = "abc123",
lastModified = "2019-12-31T23:59:59+01:00",
),
numDays = 23,
subtitle = "My subtitle",
timeZoneName = "Europe/Berlin",
Expand All @@ -31,7 +37,10 @@ class MetaExtensionsTest {
)

private val metaNetworkModel = MetaNetworkModel(
httpHeader = HttpHeaderNetworkModel("abc123"),
httpHeader = HttpHeaderNetworkModel(
eTag = "abc123",
lastModified = "2019-12-31T23:59:59+01:00",
),
numDays = 23,
subtitle = "My subtitle",
timeZoneName = "Europe/Berlin",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ class AppRepositoryLoadAndParseScheduleTest {
NetworkFetchScheduleResult(
httpStatus = httpStatus,
scheduleXml = "some fahrplan xml",
httpHeader = HttpHeader("a1b2bc3"),
httpHeader = HttpHeader(eTag = "a1b2bc3", lastModified = "2023-12-31T23:59:59+01:00"),
hostName = HOST_NAME
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package info.metadude.android.eventfahrplan.database.extensions
import androidx.test.ext.junit.runners.AndroidJUnit4
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.ETAG
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.NUM_DAYS
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.SCHEDULE_LAST_MODIFIED
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.SUBTITLE
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.TIME_ZONE_NAME
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.TITLE
Expand All @@ -19,7 +20,7 @@ class MetaExtensionsTest {
@Test
fun toContentValues() {
val meta = Meta(
httpHeader = HttpHeader(eTag = "abc123"),
httpHeader = HttpHeader(eTag = "abc123", lastModified = "2023-12-31T23:59:59+01:00"),
numDays = 23,
subtitle = "My subtitle",
timeZoneName = "Europe/Berlin",
Expand All @@ -28,6 +29,7 @@ class MetaExtensionsTest {
)
val values = meta.toContentValues()
assertThat(values.getAsString(ETAG)).isEqualTo("abc123")
assertThat(values.getAsString(SCHEDULE_LAST_MODIFIED)).isEqualTo("2023-12-31T23:59:59+01:00")
assertThat(values.getAsInteger(NUM_DAYS)).isEqualTo(23)
assertThat(values.getAsString(SUBTITLE)).isEqualTo("My subtitle")
assertThat(values.getAsString(TIME_ZONE_NAME)).isEqualTo("Europe/Berlin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface Columns {
/* 5 */ String ETAG = "etag";
/* 6 */ String NUM_DAYS = "numdays";
/* 7 */ String TIME_ZONE_NAME = "time_zone_name";
/* 8 */ String SCHEDULE_LAST_MODIFIED = "schedule_last_modified";
}

interface Defaults {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package info.metadude.android.eventfahrplan.database.extensions
import androidx.core.content.contentValuesOf
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.ETAG
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.NUM_DAYS
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.SCHEDULE_LAST_MODIFIED
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.SUBTITLE
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.TIME_ZONE_NAME
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.TITLE
Expand All @@ -11,6 +12,7 @@ import info.metadude.android.eventfahrplan.database.models.Meta

fun Meta.toContentValues() = contentValuesOf(
ETAG to httpHeader.eTag,
SCHEDULE_LAST_MODIFIED to httpHeader.lastModified,
NUM_DAYS to numDays,
SUBTITLE to subtitle,
TIME_ZONE_NAME to timeZoneName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ package info.metadude.android.eventfahrplan.database.models
*/
data class HttpHeader(
val eTag: String = "",
val lastModified: String = "",
)

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteException
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.ETAG
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.NUM_DAYS
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.SCHEDULE_LAST_MODIFIED
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.SUBTITLE
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.TIME_ZONE_NAME
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns.TITLE
Expand Down Expand Up @@ -54,6 +55,7 @@ class RealMetaDatabaseRepository(
subtitle = cursor.getString(SUBTITLE),
httpHeader = HttpHeader(
eTag = cursor.getString(ETAG),
lastModified = cursor.getString(SCHEDULE_LAST_MODIFIED),
),
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable;
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Columns;
import info.metadude.android.eventfahrplan.database.contract.FahrplanContract.MetasTable.Defaults;
import info.metadude.android.eventfahrplan.database.extensions.SQLiteDatabaseExtensions;

public class MetaDBOpenHelper extends SQLiteOpenHelper {

private static final int DATABASE_VERSION = 8;
private static final int DATABASE_VERSION = 9;

private static final String DATABASE_NAME = "meta";

Expand All @@ -23,7 +24,8 @@ public class MetaDBOpenHelper extends SQLiteOpenHelper {
Columns.TITLE + " TEXT, " +
Columns.SUBTITLE + " TEXT, " +
Columns.ETAG + " TEXT, " +
Columns.TIME_ZONE_NAME + " TEXT);";
Columns.TIME_ZONE_NAME + " TEXT, " +
Columns.SCHEDULE_LAST_MODIFIED + " TEXT DEFAULT '');";

public MetaDBOpenHelper(@NonNull Context context) {
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
Expand Down Expand Up @@ -64,5 +66,12 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + MetasTable.NAME);
onCreate(db);
}
if (oldVersion < 9) {
boolean columnExists = SQLiteDatabaseExtensions.columnExists(db, MetasTable.NAME, Columns.SCHEDULE_LAST_MODIFIED);
if (!columnExists) {
db.execSQL("ALTER TABLE " + MetasTable.NAME + " ADD COLUMN " +
Columns.SCHEDULE_LAST_MODIFIED + " TEXT DEFAULT ''");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal class FetchFahrplan(private val logging: Logging) {

fun fetch(okHttpClient: OkHttpClient, url: String, httpHeader: HttpHeader) {
task = FetchFahrplanTask(okHttpClient, logging, onFetchScheduleResult)
task.execute(url, httpHeader.eTag)
task.execute(url, httpHeader.eTag, httpHeader.lastModified)
}

fun cancel() {
Expand Down Expand Up @@ -55,10 +55,13 @@ internal class FetchFahrplanTask(
const val LOG_TAG = "FetchFahrplan"
const val HTTP_HEADER_NAME_ETAG = "ETag"
const val HTTP_HEADER_NAME_IF_NONE_MATCH = "If-None-Match"
const val HTTP_HEADER_NAME_LAST_MODIFIED = "Last-Modified"
const val HTTP_HEADER_NAME_IF_MODIFIED_SINCE = "If-Modified-Since"
}

private var responseStr = EMPTY_RESPONSE_STRING
private var eTagStr = ""
private var lastModifiedStr = ""
private var completed = false
private lateinit var status: HttpStatus
private var host = ""
Expand All @@ -75,8 +78,9 @@ internal class FetchFahrplanTask(
override fun doInBackground(vararg args: String?): HttpStatus {
val url = args[0]!!
val eTag = args[1]!!
val lastModified = args[2]!!
host = Uri.parse(url).host ?: throw NullPointerException("Host is null for url = '$url'")
return fetch(url, eTag)
return fetch(url, eTag, lastModified)
}

@Deprecated("Deprecated in Java")
Expand All @@ -95,27 +99,31 @@ internal class FetchFahrplanTask(
if (status == HttpStatus.HTTP_OK) {
logging.d(LOG_TAG, "Fetch done successfully")
onFetchScheduleResult(
FetchScheduleResult(status, responseStr, HttpHeader(eTagStr), host, exceptionMessage)
FetchScheduleResult(status, responseStr, HttpHeader(eTagStr, lastModifiedStr), host, exceptionMessage)
)
} else {
logging.d(LOG_TAG, "Fetch failed")
onFetchScheduleResult(
FetchScheduleResult(
status, EMPTY_RESPONSE_STRING, HttpHeader(eTagStr), host, exceptionMessage
status, EMPTY_RESPONSE_STRING, HttpHeader(eTagStr, lastModifiedStr), host, exceptionMessage
)
)
}
completed = false // notify only once
}

private fun fetch(url: String, eTag: String): HttpStatus {
private fun fetch(url: String, eTag: String, lastModified: String): HttpStatus {
logging.d(LOG_TAG, url)
logging.d(LOG_TAG, "$HTTP_HEADER_NAME_ETAG: '$eTag'")
logging.d(LOG_TAG, "$HTTP_HEADER_NAME_LAST_MODIFIED: '$lastModified'")
val requestBuilder = Request.Builder().apply {
url(url)
if (eTag.isNotEmpty()) {
addHeader(HTTP_HEADER_NAME_IF_NONE_MATCH, eTag)
}
if (lastModified.isNotEmpty()) {
addHeader(HTTP_HEADER_NAME_IF_MODIFIED_SINCE, lastModified)
}
}

val response = try {
Expand Down Expand Up @@ -162,6 +170,13 @@ internal class FetchFahrplanTask(
logging.d(LOG_TAG, "$HTTP_HEADER_NAME_ETAG: '$eTagStr'")
}

lastModifiedStr = response.header(HTTP_HEADER_NAME_LAST_MODIFIED).orEmpty()
if (lastModifiedStr.isEmpty()) {
logging.d(LOG_TAG, "$HTTP_HEADER_NAME_LAST_MODIFIED is missing")
} else {
logging.d(LOG_TAG, "$HTTP_HEADER_NAME_LAST_MODIFIED: '$lastModifiedStr'")
}

responseStr = try {
response.body!!.string()
} catch (e: NullPointerException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import info.metadude.android.eventfahrplan.network.fetching.FetchFahrplan
*/
data class HttpHeader(
val eTag: String = "",
val lastModified: String = "",
)
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public FahrplanParser(@NonNull Logging logging) {

public void parse(String fahrplan, HttpHeader httpHeader) {
task = new ParserTask(logging, listener);
task.execute(fahrplan, httpHeader.getETag());
task.execute(fahrplan, httpHeader.getETag(), httpHeader.getLastModified());
}

public void cancel() {
Expand Down Expand Up @@ -98,7 +98,7 @@ public void setListener(FahrplanParser.OnParseCompleteListener listener) {

@Override
protected Boolean doInBackground(String... args) {
boolean parsingSuccessful = parseFahrplan(args[0], args[1]);
boolean parsingSuccessful = parseFahrplan(args[0], args[1], args[2]);
if (parsingSuccessful) {
DateFieldValidation dateFieldValidation = new DateFieldValidation(logging);
dateFieldValidation.validate(sessions);
Expand Down Expand Up @@ -126,7 +126,7 @@ protected void onPostExecute(Boolean result) {
}
}

private Boolean parseFahrplan(String fahrplan, String eTag) {
private Boolean parseFahrplan(String fahrplan, String eTag, String lastModified) {
XmlPullParser parser = Xml.newPullParser();
try {
parser.setInput(new StringReader(fahrplan));
Expand All @@ -148,7 +148,7 @@ private Boolean parseFahrplan(String fahrplan, String eTag) {
case XmlPullParser.START_DOCUMENT:
sessions = new ArrayList<>();
meta = new Meta();
meta.setHttpHeader(new HttpHeader(eTag));
meta.setHttpHeader(new HttpHeader(eTag, lastModified));
break;
case XmlPullParser.END_TAG:
name = parser.getName();
Expand Down

0 comments on commit 0d997a5

Please sign in to comment.