Loading src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt 0 → 100644 +60 −0 Original line number Diff line number Diff line package at.bitfire.ical4android import androidx.test.filters.SmallTest import net.fortuna.ical4j.model.parameter.Rsvp import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.Uid import org.json.JSONException import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test class UnknownPropertyTest { @Test @SmallTest fun testFromExtendedProperty() { val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"UID\", \"PropValue\" ]") assertTrue(prop is Uid) assertEquals("UID", prop.name) assertEquals("PropValue", prop.value) } @Test @SmallTest fun testFromExtendedPropertyWithParameters() { val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"ATTENDEE\", \"PropValue\", { \"x-param1\": \"value1\", \"x-param2\": \"value2\" } ]") assertTrue(prop is Attendee) assertEquals("ATTENDEE", prop.name) assertEquals("PropValue", prop.value) assertEquals(2, prop.parameters.size()) assertEquals("value1", prop.parameters.getParameter("x-param1").value) assertEquals("value2", prop.parameters.getParameter("x-param2").value) } @Test(expected = JSONException::class) @SmallTest fun testFromInvalidExtendedProperty() { AndroidEvent.UnknownProperty.fromExtendedProperty("This isn't JSON") } @Test @SmallTest fun testToExtendedProperty() { val attendee = Attendee("mailto:test@test.at") assertEquals( "ATTENDEE:mailto:test@test.at", attendee.toString().trim() ) attendee.parameters.add(Rsvp(true)) attendee.parameters.add(XParameter("X-My-Param", "SomeValue")) assertEquals( "ATTENDEE;RSVP=TRUE;X-My-Param=SomeValue:mailto:test@test.at", attendee.toString().trim() ) } } No newline at end of file src/main/java/at/bitfire/ical4android/AndroidEvent.kt +87 −27 Original line number Diff line number Diff line Loading @@ -26,7 +26,11 @@ import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.* import net.fortuna.ical4j.model.property.* import net.fortuna.ical4j.util.TimeZones import java.io.* import org.json.JSONArray import org.json.JSONObject import java.io.ByteArrayInputStream import java.io.FileNotFoundException import java.io.ObjectInputStream import java.net.URI import java.net.URISyntaxException import java.text.ParseException Loading @@ -50,7 +54,9 @@ abstract class AndroidEvent( companion object { /** [ExtendedProperties.NAME] for unknown iCal properties */ @Deprecated("New serialization format", ReplaceWith("EXT_UNKNOWN_PROPERTY2")) const val EXT_UNKNOWN_PROPERTY = "unknown-property" const val EXT_UNKNOWN_PROPERTY2 = "unknown-property.v2" const val MAX_UNKNOWN_PROPERTY_SIZE = 25000 // not declared in ical4j Parameters class yet Loading Loading @@ -325,17 +331,25 @@ abstract class AndroidEvent( protected open fun populateExtended(row: ContentValues) { Constants.log.log(Level.FINE, "Read extended property from calender provider", row.getAsString(ExtendedProperties.NAME)) val event = requireNotNull(event) if (row.getAsString(ExtendedProperties.NAME) == EXT_UNKNOWN_PROPERTY) { // de-serialize unknown property val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP)) try { when (row.getAsString(ExtendedProperties.NAME)) { EXT_UNKNOWN_PROPERTY -> { // deserialize unknown property v1 (deprecated) val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP)) ObjectInputStream(stream).use { event!!.unknownProperties += it.readObject() as Property event.unknownProperties += it.readObject() as Property } } catch(e: Exception) { Constants.log.log(Level.WARNING, "Couldn't de-serialize unknown property", e) } EXT_UNKNOWN_PROPERTY2 -> { // deserialize unknown property v2 event.unknownProperties += UnknownProperty.fromExtendedProperty(row.getAsString(ExtendedProperties.VALUE)) } } } catch(e: Exception) { Constants.log.log(Level.WARNING, "Couldn't parse extended property", e) } } Loading Loading @@ -690,26 +704,17 @@ abstract class AndroidEvent( } protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int, property: Property) { val baos = ByteArrayOutputStream() try { ObjectOutputStream(baos).use { oos -> oos.writeObject(property) if (baos.size() > MAX_UNKNOWN_PROPERTY_SIZE) { Constants.log.warning("Ignoring unknown property with ${baos.size()} octets") if (property.value.length > MAX_UNKNOWN_PROPERTY_SIZE) { Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") return } val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI)) builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY) .withValue(ExtendedProperties.VALUE, Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP)) builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY2) .withValue(ExtendedProperties.VALUE, UnknownProperty.toExtendedProperty(property)) batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent)) } } catch(e: IOException) { Constants.log.log(Level.WARNING, "Couldn't serialize unknown property", e) } } private fun useRetainedClassification() { val event = requireNotNull(event) Loading Loading @@ -737,7 +742,62 @@ abstract class AndroidEvent( return calendar.syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)) } override fun toString() = MiscUtils.reflectionToString(this) /** * Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row. * * Format: `{ propertyName, propertyValue, { param1Name: param1Value, ... } }`, with the third * array (parameters) being optional. */ object UnknownProperty { /** * Deserializes a JSON string from an ExtendedProperty value to an ical4j property. * * @param jsonString JSON representation of an ical4j property * @return ical4j property, generated from [jsonString] * @throws org.json.JSONException when the input value can't be parsed */ fun fromExtendedProperty(jsonString: String): Property { val json = JSONArray(jsonString) val name = json.getString(0) val value = json.getString(1) val params = ParameterList() json.optJSONObject(2)?.let { jsonParams -> for (paramName in jsonParams.keys()) params.add(ICalendar.parameterFactoryRegistry.createParameter( paramName, jsonParams.getString(paramName) )) } return ICalendar.propertyFactoryRegistry.createProperty(name, params, value) } /** * Serializes an ical4j property to a JSON string that can be stored in an ExtendedProperty. * * @param prop property to serialize as JSON * @return JSON representation of [prop] */ fun toExtendedProperty(prop: Property): String { val json = JSONArray() json.put(prop.name) json.put(prop.value) if (!prop.parameters.isEmpty) { val jsonParams = JSONObject() for (param in prop.parameters) jsonParams.put(param.name, param.value) json.put(jsonParams) } return json.toString() } } } src/main/java/at/bitfire/ical4android/ICalendar.kt +2 −2 Original line number Diff line number Diff line Loading @@ -38,8 +38,8 @@ open class ICalendar { } var prodId = ProdId("+//IDN bitfire.at//ical4android") private val propertyFactoryRegistry = PropertyFactoryRegistry() private val parameterFactoryRegistry = ParameterFactoryRegistry() val propertyFactoryRegistry = PropertyFactoryRegistry() val parameterFactoryRegistry = ParameterFactoryRegistry() @JvmStatic protected fun calendarBuilder() = CalendarBuilder( Loading Loading
src/androidTest/java/at/bitfire/ical4android/UnknownPropertyTest.kt 0 → 100644 +60 −0 Original line number Diff line number Diff line package at.bitfire.ical4android import androidx.test.filters.SmallTest import net.fortuna.ical4j.model.parameter.Rsvp import net.fortuna.ical4j.model.parameter.XParameter import net.fortuna.ical4j.model.property.Attendee import net.fortuna.ical4j.model.property.Uid import org.json.JSONException import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test class UnknownPropertyTest { @Test @SmallTest fun testFromExtendedProperty() { val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"UID\", \"PropValue\" ]") assertTrue(prop is Uid) assertEquals("UID", prop.name) assertEquals("PropValue", prop.value) } @Test @SmallTest fun testFromExtendedPropertyWithParameters() { val prop = AndroidEvent.UnknownProperty.fromExtendedProperty("[ \"ATTENDEE\", \"PropValue\", { \"x-param1\": \"value1\", \"x-param2\": \"value2\" } ]") assertTrue(prop is Attendee) assertEquals("ATTENDEE", prop.name) assertEquals("PropValue", prop.value) assertEquals(2, prop.parameters.size()) assertEquals("value1", prop.parameters.getParameter("x-param1").value) assertEquals("value2", prop.parameters.getParameter("x-param2").value) } @Test(expected = JSONException::class) @SmallTest fun testFromInvalidExtendedProperty() { AndroidEvent.UnknownProperty.fromExtendedProperty("This isn't JSON") } @Test @SmallTest fun testToExtendedProperty() { val attendee = Attendee("mailto:test@test.at") assertEquals( "ATTENDEE:mailto:test@test.at", attendee.toString().trim() ) attendee.parameters.add(Rsvp(true)) attendee.parameters.add(XParameter("X-My-Param", "SomeValue")) assertEquals( "ATTENDEE;RSVP=TRUE;X-My-Param=SomeValue:mailto:test@test.at", attendee.toString().trim() ) } } No newline at end of file
src/main/java/at/bitfire/ical4android/AndroidEvent.kt +87 −27 Original line number Diff line number Diff line Loading @@ -26,7 +26,11 @@ import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.parameter.* import net.fortuna.ical4j.model.property.* import net.fortuna.ical4j.util.TimeZones import java.io.* import org.json.JSONArray import org.json.JSONObject import java.io.ByteArrayInputStream import java.io.FileNotFoundException import java.io.ObjectInputStream import java.net.URI import java.net.URISyntaxException import java.text.ParseException Loading @@ -50,7 +54,9 @@ abstract class AndroidEvent( companion object { /** [ExtendedProperties.NAME] for unknown iCal properties */ @Deprecated("New serialization format", ReplaceWith("EXT_UNKNOWN_PROPERTY2")) const val EXT_UNKNOWN_PROPERTY = "unknown-property" const val EXT_UNKNOWN_PROPERTY2 = "unknown-property.v2" const val MAX_UNKNOWN_PROPERTY_SIZE = 25000 // not declared in ical4j Parameters class yet Loading Loading @@ -325,17 +331,25 @@ abstract class AndroidEvent( protected open fun populateExtended(row: ContentValues) { Constants.log.log(Level.FINE, "Read extended property from calender provider", row.getAsString(ExtendedProperties.NAME)) val event = requireNotNull(event) if (row.getAsString(ExtendedProperties.NAME) == EXT_UNKNOWN_PROPERTY) { // de-serialize unknown property val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP)) try { when (row.getAsString(ExtendedProperties.NAME)) { EXT_UNKNOWN_PROPERTY -> { // deserialize unknown property v1 (deprecated) val stream = ByteArrayInputStream(Base64.decode(row.getAsString(ExtendedProperties.VALUE), Base64.NO_WRAP)) ObjectInputStream(stream).use { event!!.unknownProperties += it.readObject() as Property event.unknownProperties += it.readObject() as Property } } catch(e: Exception) { Constants.log.log(Level.WARNING, "Couldn't de-serialize unknown property", e) } EXT_UNKNOWN_PROPERTY2 -> { // deserialize unknown property v2 event.unknownProperties += UnknownProperty.fromExtendedProperty(row.getAsString(ExtendedProperties.VALUE)) } } } catch(e: Exception) { Constants.log.log(Level.WARNING, "Couldn't parse extended property", e) } } Loading Loading @@ -690,26 +704,17 @@ abstract class AndroidEvent( } protected open fun insertUnknownProperty(batch: BatchOperation, idxEvent: Int, property: Property) { val baos = ByteArrayOutputStream() try { ObjectOutputStream(baos).use { oos -> oos.writeObject(property) if (baos.size() > MAX_UNKNOWN_PROPERTY_SIZE) { Constants.log.warning("Ignoring unknown property with ${baos.size()} octets") if (property.value.length > MAX_UNKNOWN_PROPERTY_SIZE) { Constants.log.warning("Ignoring unknown property with ${property.value.length} octets (too long)") return } val builder = ContentProviderOperation.newInsert(calendar.syncAdapterURI(ExtendedProperties.CONTENT_URI)) builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY) .withValue(ExtendedProperties.VALUE, Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP)) builder .withValue(ExtendedProperties.NAME, EXT_UNKNOWN_PROPERTY2) .withValue(ExtendedProperties.VALUE, UnknownProperty.toExtendedProperty(property)) batch.enqueue(BatchOperation.Operation(builder, ExtendedProperties.EVENT_ID, idxEvent)) } } catch(e: IOException) { Constants.log.log(Level.WARNING, "Couldn't serialize unknown property", e) } } private fun useRetainedClassification() { val event = requireNotNull(event) Loading Loading @@ -737,7 +742,62 @@ abstract class AndroidEvent( return calendar.syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)) } override fun toString() = MiscUtils.reflectionToString(this) /** * Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row. * * Format: `{ propertyName, propertyValue, { param1Name: param1Value, ... } }`, with the third * array (parameters) being optional. */ object UnknownProperty { /** * Deserializes a JSON string from an ExtendedProperty value to an ical4j property. * * @param jsonString JSON representation of an ical4j property * @return ical4j property, generated from [jsonString] * @throws org.json.JSONException when the input value can't be parsed */ fun fromExtendedProperty(jsonString: String): Property { val json = JSONArray(jsonString) val name = json.getString(0) val value = json.getString(1) val params = ParameterList() json.optJSONObject(2)?.let { jsonParams -> for (paramName in jsonParams.keys()) params.add(ICalendar.parameterFactoryRegistry.createParameter( paramName, jsonParams.getString(paramName) )) } return ICalendar.propertyFactoryRegistry.createProperty(name, params, value) } /** * Serializes an ical4j property to a JSON string that can be stored in an ExtendedProperty. * * @param prop property to serialize as JSON * @return JSON representation of [prop] */ fun toExtendedProperty(prop: Property): String { val json = JSONArray() json.put(prop.name) json.put(prop.value) if (!prop.parameters.isEmpty) { val jsonParams = JSONObject() for (param in prop.parameters) jsonParams.put(param.name, param.value) json.put(jsonParams) } return json.toString() } } }
src/main/java/at/bitfire/ical4android/ICalendar.kt +2 −2 Original line number Diff line number Diff line Loading @@ -38,8 +38,8 @@ open class ICalendar { } var prodId = ProdId("+//IDN bitfire.at//ical4android") private val propertyFactoryRegistry = PropertyFactoryRegistry() private val parameterFactoryRegistry = ParameterFactoryRegistry() val propertyFactoryRegistry = PropertyFactoryRegistry() val parameterFactoryRegistry = ParameterFactoryRegistry() @JvmStatic protected fun calendarBuilder() = CalendarBuilder( Loading