diff options
author | Ricki Hirner <hirner@bitfire.at> | 2021-07-30 19:15:07 +0300 |
---|---|---|
committer | Ricki Hirner <hirner@bitfire.at> | 2021-07-31 12:15:59 +0300 |
commit | 07f62f52cc374c584e9263a25c72cf10a54b6dc7 (patch) | |
tree | 976b6c3084751b034abd6df3d0b9946e0022f38e | |
parent | f95c5b0fb29346976b319282c514496b50335e12 (diff) |
Better conversion of Contact <-> VCard
* add converter classes: ContactReader, ContactWriter
* consequently use custom scribes whenever possible
* add tests
27 files changed, 1921 insertions, 695 deletions
@@ -18,23 +18,23 @@ by Google LLC. Android is a trademark of Google LLC._ Generated KDoc: https://bitfireAT.gitlab.io/vcard4android/dokka/vcard4android/ -Discussion: https://forums.bitfire.at/category/18/libraries - ## Contact ``` -bitfire web engineering – Stockmann, Hirner GesnbR +bitfire web engineering GmbH Florastraße 27 2540 Bad Vöslau, AUSTRIA ``` -Email: [play@bitfire.at](mailto:play@bitfire.at) (do not use this) +Email: [play@bitfire.at](mailto:play@bitfire.at) (please do not use this) + +Discussion: https://forums.bitfire.at/category/18/libraries ## License -Copyright (C) bitfire web engineering (Ricki Hirner, Bernhard Stockmann). +Copyright (C) Ricki Hirner (bitfire web engineering GmbH) and contributors. This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.html). diff --git a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt index 2244a26..5afd43d 100644 --- a/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt +++ b/src/androidTest/java/at/bitfire/vcard4android/AndroidContactTest.kt @@ -18,6 +18,7 @@ import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import at.bitfire.vcard4android.impl.TestAddressBook +import at.bitfire.vcard4android.property.XAbDate import ezvcard.VCardVersion import ezvcard.parameter.EmailType import ezvcard.property.Address @@ -75,6 +76,7 @@ class AndroidContactTest { vcard.phoneticMiddleName = "Mittelerde" vcard.phoneticFamilyName = "Fämilie" vcard.birthDay = Birthday(SimpleDateFormat("yyyy-MM-dd").parse("1980-04-16")) + vcard.customDates += LabeledProperty(XAbDate(PartialDate.parse("--0102")), "Custom Date") vcard.photo = samplePhoto val contact = AndroidContact(addressBook, vcard, null, null) @@ -92,6 +94,7 @@ class AndroidContactTest { assertEquals(vcard.phoneticMiddleName, vcard2.phoneticMiddleName) assertEquals(vcard.phoneticFamilyName, vcard2.phoneticFamilyName) assertEquals(vcard.birthDay, vcard2.birthDay) + assertArrayEquals(vcard.customDates.toArray(), vcard2.customDates.toArray()) assertNotNull(vcard.photo) } finally { contact2.delete() @@ -179,8 +182,7 @@ class AndroidContactTest { * So, ADR value components may contain DQUOTE (0x22) and don't have to be encoded as defined in RFC 6868 */ val os = ByteArrayOutputStream() - contact.write(VCardVersion.V4_0, GroupMethod.GROUP_VCARDS, os) - Constants.log.info(os.toString()) + contact.writeVCard(VCardVersion.V4_0, GroupMethod.GROUP_VCARDS, os) assertTrue(os.toString().contains("ADR;LABEL=My ^'Label^'\\nLine 2:;;Street \"Address\";;;;")) } diff --git a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt index acce3b6..2d5172d 100644 --- a/src/main/java/at/bitfire/vcard4android/AndroidContact.kt +++ b/src/main/java/at/bitfire/vcard4android/AndroidContact.kt @@ -26,6 +26,7 @@ import android.provider.ContactsContract.CommonDataKinds.StructuredName import android.provider.ContactsContract.RawContacts import android.provider.ContactsContract.RawContacts.Data import androidx.annotation.CallSuper +import at.bitfire.vcard4android.property.XAbDate import ezvcard.parameter.* import ezvcard.property.* import ezvcard.util.PartialDate @@ -456,6 +457,12 @@ open class AndroidContact( contact!!.anniversary = if (full != null) Anniversary(full) else Anniversary(partial) Event.TYPE_BIRTHDAY -> contact!!.birthDay = if (full != null) Birthday(full) else Birthday(partial) + Event.TYPE_OTHER, + Event.TYPE_CUSTOM -> { + val abDate = if (full != null) XAbDate(full) else XAbDate(partial) + val label = StringUtils.trimToNull(row.getAsString(Event.LABEL)) + contact!!.customDates += LabeledProperty(abDate, label) + } } } @@ -641,6 +648,11 @@ open class AndroidContact( contact.anniversary?.let { insertEvent(batch, Event.TYPE_ANNIVERSARY, it) } contact.birthDay?.let { insertEvent(batch, Event.TYPE_BIRTHDAY, it) } + for (customDate in contact.customDates) + if (customDate.label == null) + insertEvent(batch, Event.TYPE_OTHER, customDate.property) + else + insertEvent(batch, Event.TYPE_CUSTOM, customDate.property, label = customDate.label) } protected open fun insertStructuredName(batch: BatchOperation) { @@ -1001,7 +1013,7 @@ open class AndroidContact( batch.enqueue(builder) } - protected open fun insertEvent(batch: BatchOperation, type: Int, dateOrTime: DateOrTimeProperty) { + protected open fun insertEvent(batch: BatchOperation, type: Int, dateOrTime: DateOrTimeProperty, label: String? = null) { val dateStr: String dateStr = when { dateOrTime.date != null -> { @@ -1020,6 +1032,10 @@ open class AndroidContact( .withValue(Event.MIMETYPE, Event.CONTENT_ITEM_TYPE) .withValue(Event.TYPE, type) .withValue(Event.START_DATE, dateStr) + + if (label != null) + builder.withValue(Event.LABEL, label) + batch.enqueue(builder) } diff --git a/src/main/java/at/bitfire/vcard4android/Contact.kt b/src/main/java/at/bitfire/vcard4android/Contact.kt index 7ccf52c..e94322a 100644 --- a/src/main/java/at/bitfire/vcard4android/Contact.kt +++ b/src/main/java/at/bitfire/vcard4android/Contact.kt @@ -8,29 +8,32 @@ package at.bitfire.vcard4android -import at.bitfire.vcard4android.property.* -import ezvcard.Ezvcard -import ezvcard.VCard +import at.bitfire.vcard4android.property.CustomScribes +import at.bitfire.vcard4android.property.XAbDate import ezvcard.VCardVersion import ezvcard.io.text.VCardReader -import ezvcard.io.text.VCardWriter import ezvcard.parameter.EmailType -import ezvcard.parameter.ImageType import ezvcard.parameter.TelephoneType import ezvcard.property.* -import ezvcard.util.PartialDate -import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.builder.HashCodeBuilder import org.apache.commons.lang3.builder.ReflectionToStringBuilder import java.io.IOException import java.io.OutputStream import java.io.Reader -import java.net.URI -import java.net.URISyntaxException import java.util.* -import java.util.concurrent.atomic.AtomicInteger -import java.util.logging.Level +/** + * Data class for a contact; between vCards and the Android contacts provider. + * + * Data shall be stored without workarounds in the most appropriate form. For instance, + * an anniversary should be stored as [anniversary] and not in [customDates] with a + * proprietary label ([Contact.DATE_LABEL_ANNIVERSARY]). + * + * vCards are parsed to [Contact]s by [ContactReader]. + * [Contact]s are written to vCards by [ContactWriter]. + * + * [Contact]s are written to and read from the Android storage by [AndroidContact]. + */ class Contact { var uid: String? = null @@ -69,7 +72,7 @@ class Contact { var anniversary: Anniversary? = null var birthDay: Birthday? = null - val customDates = LinkedList<LabeledProperty<DateOrTimeProperty>>() + val customDates = LinkedList<LabeledProperty<XAbDate>>() var photo: ByteArray? = null @@ -78,22 +81,11 @@ class Contact { companion object { - - /** list of all custom scribes (will be registered to readers/writers) **/ - val customScribes = arrayOf( - AbLabel.Scribe, - AddressBookServerKind.Scribe, - AddressBookServerMember.Scribe, - PhoneticFirstName.Scribe, - PhoneticMiddleName.Scribe, - PhoneticLastName.Scribe - ) - // productID (if set) will be used to generate a PRODID property. // You may set this statically from the calling application. var productID: String? = null - const val PROPERTY_SIP = "X-SIP" + const val LABEL_GROUP_PREFIX = "item" // TEL x-types to store Android types val PHONE_TYPE_CALLBACK = TelephoneType.get("x-callback")!! @@ -118,6 +110,9 @@ class Contact { const val DATE_PARAMETER_OMIT_YEAR = "X-APPLE-OMIT-YEAR" const val DATE_PARAMETER_OMIT_YEAR_DEFAULT = 1604 + const val DATE_LABEL_ANNIVERSARY = "_\$!<Anniversary>!\$_" + const val DATE_LABEL_OTHER = "_\$!<Other>!\$_" + /** * Parses an InputStream that contains a vCard. @@ -129,391 +124,24 @@ class Contact { * @throws ezvcard.io.CannotParseException when the vCard can't be parsed */ fun fromReader(reader: Reader, downloader: Downloader?): List<Contact> { - // create new vCard reader and add custom scribes to tell the reader how to read custom properties + // create new vCard reader and add custom scribes val vCardReader = VCardReader(reader, VCardVersion.V3_0) // CardDAV requires vCard 3 or newer - for (scribe in customScribes) - vCardReader.scribeIndex.register(scribe) - - val vcards = vCardReader.readAll() - val contacts = LinkedList<Contact>() - vcards?.forEach { contacts += fromVCard(it, downloader) } - return contacts - } - - private fun fromVCard(vCard: VCard, downloader: Downloader?): Contact { - val c = Contact() - - // get X-ABLabels - val labels = vCard.getProperties(AbLabel::class.java) - - fun findAndRemoveLabel(group: String?): String? { - if (group == null) - return null - - for (label in labels) { - if (label.group.equals(group, true)) { - vCard.removeProperty(label) - return label.value - } - } - - return null - } - - // process standard properties - val toRemove = LinkedList<VCardProperty>() - for (prop in vCard.properties) { - var remove = true - when (prop) { - is Uid -> c.uid = uriToUID(prop.value) - - is Kind, is AddressBookServerKind -> { - val kindProp = prop as Kind - c.group = kindProp.isGroup - } - is Member, is AddressBookServerMember -> { - val uriProp = prop as Member - uriToUID(uriProp.uri)?.let { c.members += it } - } - - is FormattedName -> c.displayName = prop.value.trim() - is PhoneticFirstName -> c.phoneticGivenName = StringUtils.trimToNull(prop.value) - is PhoneticMiddleName -> c.phoneticMiddleName = StringUtils.trimToNull(prop.value) - is PhoneticLastName -> c.phoneticFamilyName = StringUtils.trimToNull(prop.value) - is StructuredName -> { - c.prefix = StringUtils.trimToNull(prop.prefixes.joinToString(" ")) - c.givenName = StringUtils.trimToNull(prop.given) - c.middleName = StringUtils.trimToNull(prop.additionalNames.joinToString(" ")) - c.familyName = StringUtils.trimToNull(prop.family) - c.suffix = StringUtils.trimToNull(prop.suffixes.joinToString(" ")) - } - is Nickname -> c.nickName = LabeledProperty(prop, findAndRemoveLabel(prop.group)) - - is Organization -> c.organization = prop - is Title -> c.jobTitle = StringUtils.trimToNull(prop.value) - is Role -> c.jobDescription = StringUtils.trimToNull(prop.value) - - is Telephone -> if (!prop.text.isNullOrBlank()) - c.phoneNumbers += LabeledProperty(prop, findAndRemoveLabel(prop.group)) - is Email -> if (!prop.value.isNullOrBlank()) - c.emails += LabeledProperty(prop, findAndRemoveLabel(prop.group)) - is Impp -> c.impps += LabeledProperty(prop, findAndRemoveLabel(prop.group)) - is Address -> c.addresses += LabeledProperty(prop, findAndRemoveLabel(prop.group)) - - is Note -> c.note = if (c.note.isNullOrEmpty()) prop.value else "${c.note}\n\n\n${prop.value}" - is Url -> c.urls += LabeledProperty(prop, findAndRemoveLabel(prop.group)) - is Categories -> c.categories.addAll(prop.values) - - is Birthday -> c.birthDay = checkVCard3PartialDate(prop) - is Anniversary -> c.anniversary = checkVCard3PartialDate(prop) - - is Related -> StringUtils.trimToNull(prop.text)?.let { c.relations += prop } - is Photo -> - c.photo = prop.data ?: prop.url?.let { url -> - downloader?.let { - Constants.log.info("Downloading photo from $url") - it.download(url, "image/*") - } - } - - // remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions - is Logo, is Sound -> { /* remove = true */ } - - // remove properties that don't apply anymore - is ProductId, - is Revision, - is SortString, - is Source -> { - /* remove = true */ - } - - else -> remove = false // don't remove unknown properties - } - - if (remove) - toRemove += prop - } - toRemove.forEach { vCard.removeProperty(it) } - - // process extended properties - val extToRemove = LinkedList<RawProperty>() - for (prop in vCard.extendedProperties) { - var remove = true - when (prop.propertyName) { - PROPERTY_SIP -> c.impps += LabeledProperty(Impp("sip", prop.value), findAndRemoveLabel(prop.group)) - else -> remove = false // don't remove unknown extended properties - } - - if (remove) - extToRemove += prop - } - extToRemove.distinct().forEach { vCard.removeExtendedProperty(it.propertyName) } + CustomScribes.registerAt(vCardReader) + val vCards = vCardReader.readAll() - if (c.uid == null) { - Constants.log.warning("Received vCard without UID, generating new one") - c.uid = UUID.randomUUID().toString() - } - - // remove properties which - // - couldn't be parsed (and thus are treated as extended/unknown properties), and - // - must occur once max. - arrayOf("ANNIVERSARY", "BDAY", "KIND", "N", "PRODID", "REV", "UID").forEach { - vCard.removeExtendedProperty(it) - } - - // store all remaining properties into unknownProperties - if (vCard.properties.isNotEmpty() || vCard.extendedProperties.isNotEmpty()) - try { - val writer = Ezvcard.write(vCard) - for (scribe in customScribes) // unknwown properties may contain custom scribes like an unmatched X-ABLabel - writer.register(scribe) - c.unknownProperties = writer.go() - } catch(e: Exception) { - Constants.log.log(Level.WARNING, "Couldn't serialize unknown properties, dropping them", e) - } - - return c - } - - private fun uriToUID(uriString: String?): String? { - if (uriString == null) - return null - return try { - val uri = URI(uriString) - when { - uri.scheme == null -> - uri.schemeSpecificPart - uri.scheme.equals("urn", true) && uri.schemeSpecificPart.startsWith("uuid:", true) -> - uri.schemeSpecificPart.substring(5) - else -> - null - } - } catch(e: URISyntaxException) { - Constants.log.warning("Invalid URI for UID: $uriString") - uriString + return vCards.map { vCard -> + // convert every vCard to a Contact data object + ContactReader.fromVCard(vCard, downloader) } } - private fun<T: DateOrTimeProperty> checkVCard3PartialDate(property: T): T { - property.date?.let { date -> - property.getParameter(DATE_PARAMETER_OMIT_YEAR)?.let { omitYearStr -> - try { - val omitYear = Integer.parseInt(omitYearStr) - val cal = GregorianCalendar.getInstance() - cal.time = date - if (cal.get(GregorianCalendar.YEAR) == omitYear) { - val partial = PartialDate.builder() - .date(cal.get(GregorianCalendar.DAY_OF_MONTH)) - .month(cal.get(GregorianCalendar.MONTH) + 1) - .build() - property.partialDate = partial - } - } catch(e: NumberFormatException) { - Constants.log.log(Level.WARNING, "Unparseable $DATE_PARAMETER_OMIT_YEAR") - } finally { - property.removeParameter(DATE_PARAMETER_OMIT_YEAR) - } - } - } - return property - } - } @Throws(IOException::class) - fun write(vCardVersion: VCardVersion, groupMethod: GroupMethod, os: OutputStream) { - var vCard = VCard() - try { - unknownProperties?.let { vCard = Ezvcard.parse(unknownProperties).first() } - } catch (e: Exception) { - Constants.log.fine("Couldn't parse original vCard with retained properties") - } - - // UID - uid?.let { vCard.uid = Uid(it) } - // PRODID - productID?.let { vCard.setProductId(it) } - - // group support - if (group && groupMethod == GroupMethod.GROUP_VCARDS) { - if (vCardVersion == VCardVersion.V4_0) { - vCard.kind = Kind.group() - members.forEach { vCard.members += Member("urn:uuid:$it") } - } else { // "vCard4 as vCard3" (Apple-style) - vCard.setProperty(AddressBookServerKind(Kind.GROUP)) - members.forEach { vCard.addProperty(AddressBookServerMember("urn:uuid:$it")) } - } - } - - // FN - var fn = displayName - if (fn.isNullOrEmpty()) - organization?.let { - for (part in it.values) { - fn = part - if (!fn.isNullOrEmpty()) - break - } - } - if (fn.isNullOrEmpty()) - nickName?.let { fn = it.property.values.firstOrNull() } - if (fn.isNullOrEmpty()) - emails.firstOrNull()?.let { fn = it.property.value } - if (fn.isNullOrEmpty()) - phoneNumbers.firstOrNull()?.let { fn = it.property.text } - if (fn.isNullOrEmpty()) - fn = uid ?: "" - vCard.setFormattedName(fn) - - // N - if (prefix != null || familyName != null || middleName != null || givenName != null || suffix != null) { - val n = StructuredName() - prefix?.let { it.split(' ').forEach { singlePrefix -> n.prefixes += singlePrefix } } - n.given = givenName - middleName?.let { it.split(' ').forEach { singleName -> n.additionalNames += singleName } } - n.family = familyName - suffix?.let { it.split(' ').forEach { singleSuffix -> n.suffixes += singleSuffix } } - vCard.structuredName = n - - } else if (vCardVersion == VCardVersion.V3_0) { - // (only) vCard 3 requires N [RFC 2426 3.1.2] - if (group && groupMethod == GroupMethod.GROUP_VCARDS) { - val n = StructuredName() - n.family = fn - vCard.structuredName = n - } else - vCard.structuredName = StructuredName() - } - - // phonetic names - phoneticGivenName?.let { vCard.addProperty(PhoneticFirstName(it)) } - phoneticMiddleName?.let { vCard.addProperty(PhoneticMiddleName(it)) } - phoneticFamilyName?.let { vCard.addProperty(PhoneticLastName(it)) } - - // ORG, TITLE, ROLE - organization?.let { vCard.organization = it } - jobTitle?.let { vCard.addTitle(it) } - jobDescription?.let { vCard.addRole(it) } - - // will be used to count "itemXX." property groups - val labelIterator = AtomicInteger() - // TODO move function outside; make clear that it modifies labeledProperty.property - fun addLabel(labeledProperty: LabeledProperty<VCardProperty>) { - labeledProperty.label?.let { label -> - val group = "group${labelIterator.incrementAndGet()}" - labeledProperty.property.group = group - - val abLabel = AbLabel(label) - abLabel.group = group - vCard.addProperty(abLabel) - } - } - - // NICKNAME - nickName?.let { labeledNickName -> - vCard.addNickname(labeledNickName.property) - addLabel(labeledNickName) - } - - // TEL - for (labeledPhone in phoneNumbers) { - vCard.addTelephoneNumber(labeledPhone.property) - addLabel(labeledPhone) - } - - // EMAIL - for (labeledEmail in emails) { - vCard.addEmail(labeledEmail.property) - addLabel(labeledEmail) - } - - // IMPP - for (labeledImpp in impps) { - vCard.addImpp(labeledImpp.property) - addLabel(labeledImpp) - } - - // ADR - for (labeledAddress in addresses) { - val address = labeledAddress.property - vCard.addAddress(address) - addLabel(labeledAddress) - } - - // NOTE - note?.let { vCard.addNote(it) } - - // URL - for (labeledUrl in urls) { - val url = labeledUrl.property - vCard.addUrl(url) - addLabel(labeledUrl) - } - - // CATEGORIES - if (!categories.isEmpty()) { - val cat = Categories() - cat.values.addAll(categories) - vCard.categories = cat - } - - // ANNIVERSARY, BDAY - fun<T: DateOrTimeProperty> dateOrPartialDate(prop: T, generator: (Date) -> T): T? { - if (vCardVersion == VCardVersion.V4_0 || prop.date != null) - return prop - else prop.partialDate?.let { partial -> - // vCard 3: partial date with month and day, but without year - if (partial.date != null && partial.month != null) { - return if (partial.year != null) - // partial date is a complete date - prop - else { - // vCard 3: partial date with month and day, but without year - val fakeCal = GregorianCalendar.getInstance() - fakeCal.set(DATE_PARAMETER_OMIT_YEAR_DEFAULT, partial.month - 1, partial.date) - val fakeProp = generator(fakeCal.time) - fakeProp.addParameter(DATE_PARAMETER_OMIT_YEAR, DATE_PARAMETER_OMIT_YEAR_DEFAULT.toString()) - fakeProp - } - } - } - return null - } - anniversary?.let { vCard.anniversary = dateOrPartialDate(it) { time -> Anniversary(time, false) } } - birthDay?.let { vCard.birthday = dateOrPartialDate(it) { time -> Birthday(time, false) } } - - // RELATED - relations.forEach { vCard.addRelated(it) } - - // PHOTO - photo?.let { vCard.addPhoto(Photo(photo, ImageType.JPEG)) } - - // REV - vCard.revision = Revision.now() - - // validate vCard and log results - val validation = vCard.validate(vCardVersion) - if (!validation.isEmpty) { - val msgs = LinkedList<String>() - for ((key, warnings) in validation) - msgs += " * " + key.javaClass.simpleName + " - " + warnings.joinToString(" | ") - Constants.log.log(Level.WARNING, "vCard validation warnings", msgs.joinToString(",")) - } - - // generate VCARD - val writer = Ezvcard - .write(vCard) - .version(vCardVersion) - .versionStrict(false) // allow vCard4 properties in vCard3s - .caretEncoding(true) // enable RFC 6868 support - .prodId(productID == null) - - // tell the writer how to write custom properties - for (scribe in customScribes) - writer.register(scribe) - - return writer .go(os) + fun writeVCard(vCardVersion: VCardVersion, groupMethod: GroupMethod, os: OutputStream) { + val generator = ContactWriter.fromContact(this, vCardVersion, groupMethod) + generator.writeVCard(os) } @@ -525,9 +153,8 @@ class Contact { phoneticGivenName, phoneticMiddleName, phoneticFamilyName, nickName, organization, jobTitle, jobDescription, - phoneNumbers, emails, impps, addresses, - /* categories, */ urls, relations, - note, anniversary, birthDay, + phoneNumbers, emails, impps, addresses, /* categories, */ urls, relations, + note, anniversary, birthDay, customDates, photo /* unknownProperties */ ) diff --git a/src/main/java/at/bitfire/vcard4android/ContactReader.kt b/src/main/java/at/bitfire/vcard4android/ContactReader.kt new file mode 100644 index 0000000..21c2078 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/ContactReader.kt @@ -0,0 +1,269 @@ +package at.bitfire.vcard4android + +import at.bitfire.vcard4android.property.* +import ezvcard.Ezvcard +import ezvcard.VCard +import ezvcard.property.* +import ezvcard.util.PartialDate +import org.apache.commons.lang3.StringUtils +import java.net.URI +import java.net.URISyntaxException +import java.util.* +import java.util.logging.Level + +/** + * Responsible for converting a specific vCard with a specific version to + * the version-independent data class [Contact]. + * + * Attention: This class works with the original vCard and modifies it! + */ +class ContactReader internal constructor(val vCard: VCard, val downloader: Contact.Downloader? = null) { + + companion object { + + /** + * Maximum size for binary data like LOGO and SOUND data URIs. + * + * Data larger than this size has unfortunately to be dropped because of Android's IPC limits + * (max 1 MB per transaction, for all rows together). + */ + const val MAX_BINARY_DATA_SIZE = 25*1024 + + fun fromVCard(vCard: VCard, downloader: Contact.Downloader? = null) = + ContactReader(vCard, downloader).toContact() + + fun checkPartialDate(prop: DateOrTimeProperty) { + val date = prop.date + if (prop.partialDate == null && date != null) { + prop.getParameter(Contact.DATE_PARAMETER_OMIT_YEAR)?.let { omitYearStr -> + val cal = GregorianCalendar.getInstance() + cal.time = date + if (cal.get(GregorianCalendar.YEAR).toString() == omitYearStr) { + val partial = PartialDate.builder() + .date(cal.get(GregorianCalendar.DAY_OF_MONTH)) + .month(cal.get(GregorianCalendar.MONTH) + 1) + .build() + prop.partialDate = partial + } + + // X-APPLE-OMIT-YEAR not required anymore because we're now working with PartialDate + prop.removeParameter(Contact.DATE_PARAMETER_OMIT_YEAR) + } + } + } + + fun uriToUid(uriString: String?): String? { + if (uriString == null) + return null + + val uid = try { + val uri = URI(uriString) + when { + uri.scheme == null -> + uri.schemeSpecificPart + uri.scheme.equals("urn", true) && uri.schemeSpecificPart.startsWith("uuid:", true) -> + uri.schemeSpecificPart.substring(5) + else -> + uriString + } + } catch(e: URISyntaxException) { + Constants.log.warning("Invalid URI for UID: $uriString") + uriString + } + + return StringUtils.trimToNull(uid) + } + + } + + + /** + * Converts the vCard to a [Contact]. + */ + private fun toContact(): Contact { + val c = Contact() + + // process standard properties; after processing, only unknown properties will remain + for (prop in vCard.properties) { + var remove = true // assume that this property will be processed and thus shall be removed + + when (prop) { + is Uid -> + c.uid = uriToUid(prop.value) + + is Kind, is XAddressBookServerKind -> { + val kindProp = prop as Kind + c.group = kindProp.isGroup + } + is Member, is XAddressBookServerMember -> { + val uriProp = prop as Member + uriToUid(uriProp.uri)?.let { c.members += it } + } + + is FormattedName -> + c.displayName = StringUtils.trimToNull(prop.value) + is StructuredName -> { + c.prefix = StringUtils.trimToNull(prop.prefixes.joinToString(" ")) + c.givenName = StringUtils.trimToNull(prop.given) + c.middleName = StringUtils.trimToNull(prop.additionalNames.joinToString(" ")) + c.familyName = StringUtils.trimToNull(prop.family) + c.suffix = StringUtils.trimToNull(prop.suffixes.joinToString(" ")) + } + is XPhoneticFirstName -> + c.phoneticGivenName = StringUtils.trimToNull(prop.value) + is XPhoneticMiddleName -> + c.phoneticMiddleName = StringUtils.trimToNull(prop.value) + is XPhoneticLastName -> + c.phoneticFamilyName = StringUtils.trimToNull(prop.value) + is Nickname -> + c.nickName = LabeledProperty(prop, findAndRemoveLabel(prop.group)) + + is Categories -> + c.categories.addAll(prop.values) + + is Organization -> + c.organization = prop + is Title -> + c.jobTitle = StringUtils.trimToNull(prop.value) + is Role -> + c.jobDescription = StringUtils.trimToNull(prop.value) + + is Telephone -> + if (!prop.text.isNullOrBlank()) + c.phoneNumbers += LabeledProperty(prop, findAndRemoveLabel(prop.group)) + is Email -> + if (!prop.value.isNullOrBlank()) + c.emails += LabeledProperty(prop, findAndRemoveLabel(prop.group)) + is Impp -> + c.impps += LabeledProperty(prop, findAndRemoveLabel(prop.group)) + is XSip -> + // special case: treat X-SIP:address as IMPP:sip:address + c.impps += LabeledProperty(Impp.sip(prop.value)) + is Url -> + c.urls += LabeledProperty(prop, findAndRemoveLabel(prop.group)) + + is Address -> + c.addresses += LabeledProperty(prop, findAndRemoveLabel(prop.group)) + is Label -> { /* drop vCard3 formatted address because it can't be associated to a specific address */ } + + is Anniversary -> { + checkPartialDate(prop) + c.anniversary = prop + } + is Birthday -> { + checkPartialDate(prop) + c.birthDay = prop + } + is XAbDate -> { + checkPartialDate(prop) + var label = findAndRemoveLabel(prop.group) + if (label == Contact.DATE_LABEL_OTHER) // drop Apple "Other" label + label = null + + if (label == Contact.DATE_LABEL_ANNIVERSARY) { // convert custom date with Apple "Anniversary" label to real anniversary + if (prop.date != null) + c.anniversary = Anniversary(prop.date) + else if (prop.partialDate != null) + c.anniversary = Anniversary(prop.partialDate) + } else + c.customDates += LabeledProperty(prop, label) + } + + is Related -> + if (!prop.uri.isNullOrBlank() || !prop.text.isNullOrBlank()) + c.relations += prop + + is Note -> { + StringUtils.trimToNull(prop.value)?.let { note -> + if (c.note == null) + c.note = note + else + c.note += "\n\n\n" + note + } + } + + is Photo -> + c.photo = getPhotoBytes(prop) + + // drop large binary properties because of potential OutOfMemory / TransactionTooLarge exceptions + is Logo -> { + remove = prop.data != null && prop.data.size > MAX_BINARY_DATA_SIZE + } + is Sound -> { + remove = prop.data != null && prop.data.size > MAX_BINARY_DATA_SIZE + } + + // remove properties that don't apply anymore + is ProductId, + is Revision, + is SortString, // not counterpart in Android; remove it because FN/N may be changed, which would cause inconsistency + is Source -> { // when we upload a modified contact, the SOURCE would maybe point to a different version, which would cause inconsistency + /* remove = true */ + } + + else -> // unknown property, keep it in vCard in order to retain it + remove = false + } + + if (remove) + vCard.removeProperty(prop) + } + + if (c.uid == null) { + Constants.log.warning("Received vCard without UID, generating new one") + c.uid = UUID.randomUUID().toString() + } + + // remove properties which + // - couldn't be parsed (and thus are treated as extended/unknown properties), and + // - must not occur more than once + arrayOf("ANNIVERSARY", "BDAY", "KIND", "FN", "N", "PRODID", "REV", "UID").forEach { + vCard.removeExtendedProperty(it) + } + + if (vCard.properties.isNotEmpty() || vCard.extendedProperties.isNotEmpty()) + try { + val writer = Ezvcard + .write(vCard) + .prodId(false) + .version(vCard.version) + CustomScribes.registerAt(writer) + c.unknownProperties = writer.go() + } catch(e: Exception) { + Constants.log.log(Level.WARNING, "Couldn't serialize unknown properties, dropping them", e) + } + + return c + } + + + // helpers + + fun findAndRemoveLabel(group: String?): String? { + if (group == null) + return null + + for (label in vCard.getProperties(XAbLabel::class.java)) { + if (label.group.equals(group, true)) { + vCard.removeProperty(label) + return StringUtils.trimToNull(label.value) + } + } + + return null + } + + fun getPhotoBytes(photo: Photo): ByteArray? { + if (photo.data != null) + return photo.data + + val url = photo.url + if (photo.url != null && downloader != null) { + Constants.log.info("Downloading photo from $url") + return downloader.download(url, "image/*") + } + + return null + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/ContactWriter.kt b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt new file mode 100644 index 0000000..7e882c2 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/ContactWriter.kt @@ -0,0 +1,295 @@ +package at.bitfire.vcard4android + +import at.bitfire.vcard4android.property.* +import ezvcard.Ezvcard +import ezvcard.VCard +import ezvcard.VCardVersion +import ezvcard.io.text.VCardWriter +import ezvcard.parameter.ImageType +import ezvcard.property.* +import org.apache.commons.lang3.StringUtils +import java.io.OutputStream +import java.util.* +import java.util.logging.Level + +/** + * Responsible for converting the [Contact] data class (which is not version-specific) + * to the vCard that is actually sent to the server. + * + * Properties which are not supported by the target vCard version have to be converted appropriately. + */ +class ContactWriter private constructor(val contact: Contact, val version: VCardVersion, val groupMethod: GroupMethod) { + + private val unknownProperties = LinkedList<VCardProperty>() + val vCard = VCard() + + /** counter for item ID of labelled properties: 1 means "item1." etc. */ + private var currentItemId = 1 + + companion object { + + fun fromContact(contact: Contact, version: VCardVersion, groupMethod: GroupMethod) = + ContactWriter(contact, version, groupMethod) + + } + + init { + parseUnknownProperties() + addProperties() + } + + fun addProperties() { + contact.uid?.let { vCard.uid = Uid(it) } + Contact.productID?.let { vCard.setProductId(it) } + + addKindAndMembers() + + addFormattedName() + addStructuredName() + addPhoneticName() + contact.nickName?.let { nickName -> addLabeledProperty(nickName) } + + if (contact.categories.isNotEmpty()) + vCard.addCategories(Categories().apply { + values.addAll(contact.categories) + }) + + addOrganization() + + for (phone in contact.phoneNumbers) + addLabeledProperty(phone) + for (email in contact.emails) + addLabeledProperty(email) + for (impp in contact.impps) + addLabeledProperty(impp) + for (url in contact.urls) + addLabeledProperty(url) + + for (address in contact.addresses) + addLabeledProperty(address) + + addDates() + + for (relation in contact.relations) + vCard.addRelated(relation) + + contact.note?.let { note -> vCard.addNote(note) } + + for (unknownProperty in unknownProperties) + vCard.addProperty(unknownProperty) + + contact.photo?.let { photo -> vCard.addPhoto(Photo(photo, ImageType.JPEG)) } + + vCard.revision = Revision.now() + } + + private fun addDates() { + contact.birthDay?.let { birthday -> + // vCard3 doesn't support partial dates + if (version == VCardVersion.V3_0) + rewritePartialDate(birthday) + + vCard.birthday = birthday + } + + contact.anniversary?.let { anniversary -> + if (version == VCardVersion.V4_0) { + vCard.anniversary = anniversary + + } else /* version == VCardVersion.V3_0 */ { + // vCard3 doesn't support partial dates + rewritePartialDate(anniversary) + // vCard3 doesn't support ANNIVERSARY, rewrite to X-ABDate + addLabeledProperty(LabeledProperty(XAbDate(anniversary.date), Contact.DATE_LABEL_ANNIVERSARY)) + vCard.anniversary = null + } + } + + for (customDate in contact.customDates) { + rewritePartialDate(customDate.property) + addLabeledProperty(customDate) + } + } + + private fun addFormattedName() { + if (version == VCardVersion.V4_0) { + contact.displayName?.let { fn -> vCard.setFormattedName(fn) } + + } else /* version == VCardVersion.V3_0 */ { + // vCard 3 REQUIRES FN [RFC 2426 p. 29] + var fn = + // use display name, if available + StringUtils.trimToNull(contact.displayName) ?: + // no display name, try organization + contact.organization?.let { org -> + org.values.joinToString(" / ") + } ?: + // otherwise, try nickname + contact.nickName?.let { nick -> nick.property.values.firstOrNull() } ?: + // otherwise, try email address + contact.emails.firstOrNull()?.let { email -> email.property.value } ?: + // otherwise, try phone number + contact.phoneNumbers.firstOrNull()?.let { phone -> phone.property.text } ?: + // otherwise, try UID or use empty string + contact.uid ?: "" + vCard.setFormattedName(fn) + } + } + + private fun addKindAndMembers() { + if (contact.group && groupMethod == GroupMethod.GROUP_VCARDS) { + // TODO Use urn:uuid only when applicable + if (version == VCardVersion.V4_0) { // vCard4 + vCard.kind = Kind.group() + for (member in contact.members) + vCard.addMember(Member("urn:uuid:$member")) + } else { // "vCard4 as vCard3" (Apple-style) + vCard.setProperty(XAddressBookServerKind(Kind.GROUP)) + for (member in contact.members) + vCard.addProperty(XAddressBookServerMember("urn:uuid:$member")) + } + } + } + + private fun addOrganization() { + contact.organization?.let { vCard.organization = it } + contact.jobTitle?.let { vCard.addTitle(it) } + contact.jobDescription?.let { vCard.addRole(it) } + } + + private fun addStructuredName() { + val n = StructuredName() + + contact.prefix?.let { prefixesStr -> + for (prefix in prefixesStr.split(' ')) + n.prefixes += prefix + } + + n.given = contact.givenName + contact.middleName?.let { middleNamesStr -> + for (middleName in middleNamesStr.split(' ')) + n.additionalNames += middleName + } + n.family = contact.familyName + + contact.suffix?.let { suffixesStr -> + for (suffix in suffixesStr.split(' ')) + n.suffixes += suffix + } + + if (version == VCardVersion.V4_0) { + // add N only if there's some data in it + if (n.prefixes.isNotEmpty() || n.given != null || n.additionalNames.isNotEmpty() || n.family != null || n.suffixes.isNotEmpty()) + vCard.structuredName = n + + } else /* version == VCardVersion.V3_0 */ { + // vCard 3 REQUIRES N [RFC 2426 p. 29] + vCard.structuredName = n + } + } + + private fun addPhoneticName() { + contact.phoneticGivenName?.let { firstName -> + vCard.addProperty(XPhoneticFirstName(firstName)) + } + contact.phoneticMiddleName?.let { middleName -> + vCard.addProperty(XPhoneticMiddleName(middleName)) + } + contact.phoneticFamilyName?.let { lastName -> + vCard.addProperty(XPhoneticLastName(lastName)) + } + } + + + // helpers + + fun addLabeledProperty(labeledProperty: LabeledProperty<*>) { + val property = labeledProperty.property + + if (labeledProperty.label != null) { + // property with label -> group property and label with item ID + val itemId = getNextItemId() + + // 1. add property with item ID + property.group = itemId + + // 2. add label with same item ID + val label = XAbLabel(labeledProperty.label) + label.group = itemId + vCard.addProperty(label) + } + + vCard.addProperty(property) + } + + private fun getNextItemId(): String { + var id: String + + // increase ID until there is one which is not already used by an unknown property + do { + id = "item${currentItemId++}" + } while (unknownProperties.any { it.group == id }) + + return id + } + + private fun parseUnknownProperties() { + try { + contact.unknownProperties?.let { + Ezvcard.parse(it).first()?.let { vCard -> + unknownProperties.addAll(vCard.properties) + } + } + } catch (e: Exception) { + Constants.log.log(Level.WARNING, "Couldn't parse original vCard with retained properties", e) + } + } + + fun<T: DateOrTimeProperty> rewritePartialDate(prop: T) { + // vCard 3 doesn't understand partial dates: + // 1. the syntax is different (vCard 3: 2021-03-07, partial date: 20210307) + // 2. vCard 3 doesn't understand dates without year + val partial = prop.partialDate + if (version == VCardVersion.V3_0 && prop.date == null && partial != null) { + val originalYear = partial.year + val year = originalYear ?: Contact.DATE_PARAMETER_OMIT_YEAR_DEFAULT + + // use full date format + val fakeCal = GregorianCalendar.getInstance() + fakeCal.timeInMillis = 0 // reset everything, including milliseconds + fakeCal.set(year, partial.month - 1, partial.date, 0, 0, 0) + prop.setDate(fakeCal, false) + + if (originalYear == null) + prop.addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, Contact.DATE_PARAMETER_OMIT_YEAR_DEFAULT.toString()) + } + } + + + fun writeVCard(stream: OutputStream) { + // validate vCard and log results + val validation = vCard.validate(version) + if (!validation.isEmpty) { + val msgs = LinkedList<String>() + for ((key, warnings) in validation) + msgs += " * " + key?.javaClass?.simpleName + " - " + warnings?.joinToString(" | ") + Constants.log.log(Level.WARNING, "vCard validation warnings", msgs.joinToString(",")) + } + + val writer = VCardWriter(stream, version).apply { + isAddProdId = Contact.productID == null + CustomScribes.registerAt(scribeIndex) + + // include trailing semicolons for maximum compatibility + isIncludeTrailingSemicolons = true + + // use caret encoding for parameter values (RFC 6868) + isCaretEncodingEnabled = true + + isVersionStrict = false + } + writer.write(vCard) + writer.flush() + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/AbLabel.kt b/src/main/java/at/bitfire/vcard4android/property/AbLabel.kt deleted file mode 100644 index 94a7c6b..0000000 --- a/src/main/java/at/bitfire/vcard4android/property/AbLabel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package at.bitfire.vcard4android.property - -import ezvcard.io.scribe.StringPropertyScribe -import ezvcard.property.TextProperty - -class AbLabel(value: String?): TextProperty(value) { - - object Scribe : - StringPropertyScribe<AbLabel>(AbLabel::class.java, "X-ABLabel") { - - override fun _parseValue(value: String?) = AbLabel(value) - - } - -}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt b/src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt deleted file mode 100644 index 3124a75..0000000 --- a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerKind.kt +++ /dev/null @@ -1,19 +0,0 @@ -package at.bitfire.vcard4android.property - -import ezvcard.io.scribe.KindScribe -import ezvcard.io.scribe.StringPropertyScribe -import ezvcard.io.scribe.UriPropertyScribe -import ezvcard.property.Kind -import ezvcard.property.TextProperty -import ezvcard.property.UriProperty - -class AddressBookServerKind(value: String?): Kind(value) { - - object Scribe : - StringPropertyScribe<AddressBookServerKind>(AddressBookServerKind::class.java, "X-ADDRESSBOOKSERVER-KIND") { - - override fun _parseValue(value: String?) = AddressBookServerKind(value) - - } - -}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt b/src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt deleted file mode 100644 index b55ec97..0000000 --- a/src/main/java/at/bitfire/vcard4android/property/AddressBookServerMember.kt +++ /dev/null @@ -1,18 +0,0 @@ -package at.bitfire.vcard4android.property - -import ezvcard.io.scribe.StringPropertyScribe -import ezvcard.io.scribe.UriPropertyScribe -import ezvcard.property.Member -import ezvcard.property.TextProperty -import ezvcard.property.UriProperty - -class AddressBookServerMember(value: String?): Member(value) { - - object Scribe : - UriPropertyScribe<AddressBookServerMember>(AddressBookServerMember::class.java, "X-ADDRESSBOOKSERVER-MEMBER") { - - override fun _parseValue(value: String?) = AddressBookServerMember(value) - - } - -}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt b/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt new file mode 100644 index 0000000..84fc41b --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/CustomScribes.kt @@ -0,0 +1,37 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.chain.ChainingTextWriter +import ezvcard.io.scribe.ScribeIndex +import ezvcard.io.text.VCardReader + +object CustomScribes { + + /** list of all custom scribes (will be registered to readers/writers) **/ + val customScribes = arrayOf( + XAbDate.Scribe, + XAbLabel.Scribe, + XAddressBookServerKind.Scribe, + XAddressBookServerMember.Scribe, + XPhoneticFirstName.Scribe, + XPhoneticMiddleName.Scribe, + XPhoneticLastName.Scribe, + XSip.Scribe + ) + + fun registerAt(writer: ChainingTextWriter) { + for (scribe in customScribes) + writer.register(scribe) + } + + fun registerAt(index: ScribeIndex) { + for (scribe in customScribes) + index.register(scribe) + } + + fun registerAt(reader: VCardReader): VCardReader { + for (scribe in customScribes) + reader.scribeIndex.register(scribe) + return reader + } + +} diff --git a/src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt b/src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt deleted file mode 100644 index d30a153..0000000 --- a/src/main/java/at/bitfire/vcard4android/property/PhoneticFirstName.kt +++ /dev/null @@ -1,15 +0,0 @@ -package at.bitfire.vcard4android.property - -import ezvcard.io.scribe.StringPropertyScribe -import ezvcard.property.TextProperty - -class PhoneticFirstName(value: String?): TextProperty(value) { - - object Scribe : - StringPropertyScribe<PhoneticFirstName>(PhoneticFirstName::class.java, "X-PHONETIC-FIRST-NAME") { - - override fun _parseValue(value: String?) = PhoneticFirstName(value) - - } - -}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt b/src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt deleted file mode 100644 index c1dc171..0000000 --- a/src/main/java/at/bitfire/vcard4android/property/PhoneticLastName.kt +++ /dev/null @@ -1,15 +0,0 @@ -package at.bitfire.vcard4android.property - -import ezvcard.io.scribe.StringPropertyScribe -import ezvcard.property.TextProperty - -class PhoneticLastName(value: String?): TextProperty(value) { - - object Scribe : - StringPropertyScribe<PhoneticLastName>(PhoneticLastName::class.java, "X-PHONETIC-LAST-NAME") { - - override fun _parseValue(value: String?) = PhoneticLastName(value) - - } - -}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt b/src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt deleted file mode 100644 index fe50a82..0000000 --- a/src/main/java/at/bitfire/vcard4android/property/PhoneticMiddleName.kt +++ /dev/null @@ -1,15 +0,0 @@ -package at.bitfire.vcard4android.property - -import ezvcard.io.scribe.StringPropertyScribe -import ezvcard.property.TextProperty - -class PhoneticMiddleName(value: String?): TextProperty(value) { - - object Scribe : - StringPropertyScribe<PhoneticMiddleName>(PhoneticMiddleName::class.java, "X-PHONETIC-MIDDLE-NAME") { - - override fun _parseValue(value: String?) = PhoneticMiddleName(value) - - } - -}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XAbDate.kt b/src/main/java/at/bitfire/vcard4android/property/XAbDate.kt new file mode 100644 index 0000000..c3de1e5 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XAbDate.kt @@ -0,0 +1,23 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.DateOrTimePropertyScribe +import ezvcard.property.DateOrTimeProperty +import ezvcard.util.PartialDate +import java.util.* + +class XAbDate: DateOrTimeProperty { + + constructor(text: String?): super(text) + constructor(date: Date?): super(date, false) + constructor(partialDate: PartialDate?): super(partialDate) + + + object Scribe : DateOrTimePropertyScribe<XAbDate>(XAbDate::class.java, "X-ABDATE") { + + override fun newInstance(text: String?) = XAbDate(text) + override fun newInstance(calendar: Calendar?, hasTime: Boolean) = XAbDate(calendar?.time) + override fun newInstance(partialDate: PartialDate?) = XAbDate(partialDate) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt b/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt new file mode 100644 index 0000000..103f153 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XAbLabel.kt @@ -0,0 +1,15 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.property.TextProperty + +class XAbLabel(value: String?): TextProperty(value) { + + object Scribe : + StringPropertyScribe<XAbLabel>(XAbLabel::class.java, "X-ABLABEL") { + + override fun _parseValue(value: String?) = XAbLabel(value) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt new file mode 100644 index 0000000..4c4b38d --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerKind.kt @@ -0,0 +1,15 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.property.Kind + +class XAddressBookServerKind(value: String?): Kind(value) { + + object Scribe : + StringPropertyScribe<XAddressBookServerKind>(XAddressBookServerKind::class.java, "X-ADDRESSBOOKSERVER-KIND") { + + override fun _parseValue(value: String?) = XAddressBookServerKind(value) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt new file mode 100644 index 0000000..80f37c9 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XAddressBookServerMember.kt @@ -0,0 +1,15 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.UriPropertyScribe +import ezvcard.property.Member + +class XAddressBookServerMember(value: String?): Member(value) { + + object Scribe : + UriPropertyScribe<XAddressBookServerMember>(XAddressBookServerMember::class.java, "X-ADDRESSBOOKSERVER-MEMBER") { + + override fun _parseValue(value: String?) = XAddressBookServerMember(value) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt b/src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt new file mode 100644 index 0000000..05c24c6 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XPhoneticFirstName.kt @@ -0,0 +1,15 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.property.TextProperty + +class XPhoneticFirstName(value: String?): TextProperty(value) { + + object Scribe : + StringPropertyScribe<XPhoneticFirstName>(XPhoneticFirstName::class.java, "X-PHONETIC-FIRST-NAME") { + + override fun _parseValue(value: String?) = XPhoneticFirstName(value) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt b/src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt new file mode 100644 index 0000000..04a019b --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XPhoneticLastName.kt @@ -0,0 +1,15 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.property.TextProperty + +class XPhoneticLastName(value: String?): TextProperty(value) { + + object Scribe : + StringPropertyScribe<XPhoneticLastName>(XPhoneticLastName::class.java, "X-PHONETIC-LAST-NAME") { + + override fun _parseValue(value: String?) = XPhoneticLastName(value) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt b/src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt new file mode 100644 index 0000000..4e69244 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XPhoneticMiddleName.kt @@ -0,0 +1,15 @@ +package at.bitfire.vcard4android.property + +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.property.TextProperty + +class XPhoneticMiddleName(value: String?): TextProperty(value) { + + object Scribe : + StringPropertyScribe<XPhoneticMiddleName>(XPhoneticMiddleName::class.java, "X-PHONETIC-MIDDLE-NAME") { + + override fun _parseValue(value: String?) = XPhoneticMiddleName(value) + + } + +}
\ No newline at end of file diff --git a/src/main/java/at/bitfire/vcard4android/property/XSip.kt b/src/main/java/at/bitfire/vcard4android/property/XSip.kt new file mode 100644 index 0000000..30c5ad1 --- /dev/null +++ b/src/main/java/at/bitfire/vcard4android/property/XSip.kt @@ -0,0 +1,19 @@ +package at.bitfire.vcard4android.property + +import android.net.Uri +import ezvcard.io.scribe.ImppScribe +import ezvcard.io.scribe.StringPropertyScribe +import ezvcard.io.scribe.UriPropertyScribe +import ezvcard.property.Impp +import ezvcard.property.TextProperty +import ezvcard.property.UriProperty + +class XSip(value: String?): TextProperty(value) { + + object Scribe : StringPropertyScribe<XSip>(XSip::class.java, "X-SIP") { + + override fun _parseValue(value: String?) = XSip(value) + + } + +}
\ No newline at end of file diff --git a/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt b/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt new file mode 100644 index 0000000..51007dc --- /dev/null +++ b/src/test/java/at/bitfire/vcard4android/ContactReaderTest.kt @@ -0,0 +1,604 @@ +package at.bitfire.vcard4android + +import at.bitfire.vcard4android.property.* +import ezvcard.VCard +import ezvcard.VCardVersion +import ezvcard.parameter.ImageType +import ezvcard.parameter.RelatedType +import ezvcard.parameter.SoundType +import ezvcard.property.* +import ezvcard.util.PartialDate +import org.junit.Assert.* +import org.junit.Test +import java.net.URI +import java.util.* + +class ContactReaderTest { + + // test specific fields + + @Test + fun testAddress() { + val address = Address().apply { + streetAddress = "Street 101" + country = "XX" + } + val c = ContactReader.fromVCard(VCard().apply { + addAddress(address) + }) + assertEquals(LabeledProperty(address), c.addresses.first) + } + + @Test + fun testAddressLabel_vCard3() { + val c = ContactReader.fromVCard(VCard().apply { + addOrphanedLabel(Label("Formatted Address")) + }) + assertEquals(0, c.addresses.size) + assertNull(c.unknownProperties) + } + + + + @Test + fun testAnniversary() { + val date = Date(101, 6, 30, 0, 0, 0) + val c = ContactReader.fromVCard(VCard().apply { + anniversary = Anniversary(date) + }) + assertEquals(Anniversary(date), c.anniversary) + } + + + @Test + fun testBirthday_Date() { + val date = Date(101, 6, 30, 0, 0, 0) + val c = ContactReader.fromVCard(VCard().apply { + birthday = Birthday(date) + }) + assertEquals(Birthday(date), c.birthDay) + } + + @Test + fun testBirthday_vCard3_PartialDate() { + val c = ContactReader.fromVCard(VCard().apply { + birthday = Birthday(Date(0, 6, 30)).apply { + addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, "1900") + } + }) + assertEquals(Birthday(PartialDate.parse("--0730")), c.birthDay) + } + + @Test + fun testBirthday_vCard4_PartialDate() { + val b = Birthday(PartialDate.parse("--0730")) + val c = ContactReader.fromVCard(VCard().apply { + birthday = b + }) + assertEquals(b, c.birthDay) + } + + + @Test + fun testCategories() { + val cat = Categories().apply { + values.add("Cat1") + values.add("Cat2") + } + val c = ContactReader.fromVCard(VCard().apply { + addCategories(cat) + }) + assertArrayEquals(arrayOf("Cat1", "Cat2"), c.categories.toTypedArray()) + } + + + @Test + fun testEmail() { + val c = ContactReader.fromVCard(VCard().apply { + addEmail("test@example.com") + }) + assertEquals("test@example.com", c.emails.first.property.value) + } + + + @Test + fun testFn() { + val c = ContactReader.fromVCard(VCard().apply { + formattedName = FormattedName("Formatted Name") + }) + assertEquals("Formatted Name", c.displayName) + } + + + @Test + fun testImpp_Xmpp() { + val c = ContactReader.fromVCard(VCard().apply { + addImpp(Impp.xmpp("test@example.com")) + }) + assertEquals(URI("xmpp:test@example.com"), c.impps.first.property.uri) + } + + @Test + fun testImpp_XSip() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XSip("test@example.com")) + }) + assertEquals(URI("sip:test@example.com"), c.impps.first.property.uri) + } + + + @Test + fun testKind_Group() { + val c = ContactReader.fromVCard(VCard().apply { + kind = Kind.group() + }) + assertTrue(c.group) + } + + @Test + fun testKind_Individual() { + val c = ContactReader.fromVCard(VCard().apply { + kind = Kind.individual() + }) + assertFalse(c.group) + } + + + @Test + fun testLogo_Url() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply { + addLogo(Logo("https://example.com/logo.png", ImageType.PNG)) + }) + assertTrue(c.unknownProperties!!.contains("LOGO;MEDIATYPE=image/png:https://example.com/logo.png")) + } + + @Test + fun testLogo_Url_TooLarge() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply { + addLogo(Logo(ByteArray(ContactReader.MAX_BINARY_DATA_SIZE + 1), ImageType.PNG)) + }) + assertNull(c.unknownProperties) + } + + + @Test + fun testMember_Uid() { + val c = ContactReader.fromVCard(VCard().apply { + kind = Kind.group() + members += Member("member1") + }) + assertEquals("member1", c.members.first) + } + + @Test + fun testMember_Uid_Empty() { + val c = ContactReader.fromVCard(VCard().apply { + kind = Kind.group() + members += Member("") + }) + assertTrue(c.members.isEmpty()) + } + + @Test + fun testMember_UrnUiid() { + val c = ContactReader.fromVCard(VCard().apply { + kind = Kind.group() + members += Member("urn:uuid:be829cf2-4244-42f8-bd4c-ab39b4b5fcd3") + }) + assertEquals("be829cf2-4244-42f8-bd4c-ab39b4b5fcd3", c.members.first) + } + + @Test + fun testMember_UrnUiid_Empty() { + val c = ContactReader.fromVCard(VCard().apply { + kind = Kind.group() + members += Member("urn:uuid:") + }) + assertTrue(c.members.isEmpty()) + } + + + @Test + fun testN() { + val c = ContactReader.fromVCard(VCard().apply { + structuredName = StructuredName().apply { + prefixes.add("P1.") + prefixes.add("P2.") + given = "Given" + additionalNames.add("Middle1") + additionalNames.add("Middle2") + family = "Family" + suffixes.add("S1") + suffixes.add("S2") + } + }) + assertEquals("P1. P2.", c.prefix) + assertEquals("Given", c.givenName) + assertEquals("Middle1 Middle2", c.middleName) + assertEquals("Family", c.familyName) + assertEquals("S1 S2", c.suffix) + } + + + @Test + fun testNickname() { + val nick = Nickname().apply { + values.add("Nick1") + values.add("Nick2") + } + val c = ContactReader.fromVCard(VCard().apply { + addNickname(nick) + }) + assertEquals(nick, c.nickName?.property) + } + + + @Test + fun testNote() { + val c = ContactReader.fromVCard(VCard().apply { + addNote("Note 1") + addNote("Note 2") + }) + assertEquals("Note 1\n\n\nNote 2", c.note) + } + + + @Test + fun testOrganization() { + val org = Organization().apply { + values.add("Org") + values.add("Dept") + } + val c = ContactReader.fromVCard(VCard().apply { + setOrganization(org) + }) + assertEquals(org, c.organization) + } + + + @Test + fun testProdId() { + val c = ContactReader.fromVCard(VCard().apply { + productId = ProductId("Test") + }) + assertNull(c.unknownProperties) + } + + + @Test + fun testRelated_Uri() { + val rel = Related.email("112@example.com") + rel.types.add(RelatedType.EMERGENCY) + val c = ContactReader.fromVCard(VCard().apply { + addRelated(rel) + }) + assertEquals(rel, c.relations.first) + } + + @Test + fun testRelated_String() { + val rel = Related().apply { + text = "My Best Friend" + types.add(RelatedType.FRIEND) + } + val c = ContactReader.fromVCard(VCard().apply { + addRelated(rel) + }) + assertEquals(rel, c.relations.first) + } + + + @Test + fun testRev() { + val c = ContactReader.fromVCard(VCard().apply { + revision = Revision.now() + }) + assertNull(c.unknownProperties) + } + + @Test + fun testRev_Invalid() { + val c = ContactReader.fromVCard(VCard().apply { + addExtendedProperty("REV", "+invalid-format!") + }) + assertNull(c.unknownProperties) + } + + + @Test + fun testRole() { + val c = ContactReader.fromVCard(VCard().apply { + addRole("Job Description") + }) + assertEquals("Job Description", c.jobDescription) + } + + + @Test + fun testSortString() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V3_0).apply { + sortString = SortString("Harten") + }) + assertNull(c.unknownProperties) + } + + + @Test + fun testSource() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V3_0).apply { + addSource("https://example.com/sample.vcf") + }) + assertNull(c.unknownProperties) + } + + + @Test + fun testSound_Url() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply { + addSound(Sound("https://example.com/ding.wav", SoundType.WAV)) + }) + assertTrue(c.unknownProperties!!.contains("SOUND;MEDIATYPE=audio/wav:https://example.com/ding.wav")) + } + + @Test + fun testSound_Url_TooLarge() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply { + addSound(Sound(ByteArray(ContactReader.MAX_BINARY_DATA_SIZE + 1), SoundType.WAV)) + }) + assertNull(c.unknownProperties) + } + + + @Test + fun testTelephone() { + val c = ContactReader.fromVCard(VCard().apply { + addTelephoneNumber("+1 555 12345") + }) + assertEquals("+1 555 12345", c.phoneNumbers.first.property.text) + } + + + @Test + fun testTitle() { + val c = ContactReader.fromVCard(VCard().apply { + addTitle("Job Title") + }) + assertEquals("Job Title", c.jobTitle) + } + + + @Test + fun testUid() { + val c = ContactReader.fromVCard(VCard().apply { + uid = Uid("12345") + }) + assertEquals("12345", c.uid) + } + + + @Test + fun testUnkownProperty_vCard3() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V3_0).apply { + addProperty(RawProperty("FUTURE-PROPERTY", "12345")) + }) + assertEquals("BEGIN:VCARD\r\n" + + "VERSION:3.0\r\n" + + "FUTURE-PROPERTY:12345\r\n" + + "END:VCARD\r\n", c.unknownProperties) + } + + @Test + fun testUnkownProperty_vCard4() { + val c = ContactReader.fromVCard(VCard(VCardVersion.V4_0).apply { + addProperty(RawProperty("FUTURE-PROPERTY", "12345")) + }) + assertEquals("BEGIN:VCARD\r\n" + + "VERSION:4.0\r\n" + + "FUTURE-PROPERTY:12345\r\n" + + "END:VCARD\r\n", c.unknownProperties) + } + + + @Test + fun testUrl() { + val c = ContactReader.fromVCard(VCard().apply { + urls += Url("https://example.com") + }) + assertEquals("https://example.com", c.urls.first.property.value) + } + + + @Test + fun testXAbDate_WithoutLabel() { + val date = Date(101, 6, 30, 0, 0, 0) + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbDate(date)) + }) + assertEquals(LabeledProperty(XAbDate(date)), c.customDates.first) + } + + @Test + fun testXAbDate_WithLabel_AppleAnniversary() { + val date = Date(101, 6, 30, 0, 0, 0) + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbDate(date).apply { group = "test1" }) + addProperty(XAbLabel(Contact.DATE_LABEL_ANNIVERSARY).apply { group = "test1" }) + }) + assertEquals(0, c.customDates.size) + assertEquals(Anniversary(date), c.anniversary) + } + + @Test + fun testXAbDate_WithLabel_AppleOther() { + val date = Date(101, 6, 30, 0, 0, 0) + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbDate(date).apply { group = "test1" }) + addProperty(XAbLabel(Contact.DATE_LABEL_OTHER).apply { group = "test1" }) + }) + assertEquals(date, c.customDates.first.property.date) + assertNull(c.customDates.first.label) + } + + @Test + fun testXAbDate_WithLabel_Custom() { + val date = Date(101, 6, 30, 0, 0, 0) + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAbDate(date).apply { group = "test1" }) + addProperty(XAbLabel("Test 1").apply { group = "test1" }) + }) + assertEquals(date, c.customDates.first.property.date) + assertEquals("Test 1", c.customDates.first.label) + } + + + @Test + fun testXAddressBookServerKind_Group() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAddressBookServerKind(Kind.GROUP)) + }) + assertTrue(c.group) + } + + @Test + fun testXAddressBookServerKind_Individual() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XAddressBookServerKind(Kind.INDIVIDUAL)) + }) + assertFalse(c.group) + } + + + @Test + fun testXPhoneticName() { + val c = ContactReader.fromVCard(VCard().apply { + addProperty(XPhoneticFirstName("First")) + addProperty(XPhoneticMiddleName("Middle")) + addProperty(XPhoneticLastName("Last")) + }) + assertEquals("First", c.phoneticGivenName) + assertEquals("Middle", c.phoneticMiddleName) + assertEquals("Last", c.phoneticFamilyName) + } + + + // test helper methods + + @Test + fun testCheckPartialDate_Date_WithoutOmitYear() { + val date = Date(101, 6, 30) + val withDate = Anniversary(date) + ContactReader.checkPartialDate(withDate) + assertEquals(date, withDate.date) + assertNull(withDate.partialDate) + } + + @Test + fun testCheckPartialDate_Date_WithOmitYear_AnotherYear() { + val date = Date(10, 6, 30) + val withDate = Anniversary(date).apply { + addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, "2010") + } + ContactReader.checkPartialDate(withDate) + assertEquals(date, withDate.date) + assertNull(withDate.partialDate) + assertEquals(0, withDate.parameters.size()) // the year didn't match; we don't need the omit-year parameter anymore + } + + @Test + fun testCheckPartialDate_Date_WithOmitYear_SameYear() { + val date = Date(110, 6, 30) + val withDate = Anniversary(date).apply { + addParameter(Contact.DATE_PARAMETER_OMIT_YEAR, "2010") + } + ContactReader.checkPartialDate(withDate) + assertNull(withDate.date) + assertEquals(PartialDate.parse("--0730"), withDate.partialDate) + assertEquals(0, withDate.parameters.size()) + } + + @Test + fun testCheckPartialDate_PartialDate() { + val partialDate = PartialDate.parse("--0730") + val withDate = Anniversary(partialDate) + ContactReader.checkPartialDate(withDate) + assertNull(withDate.date) + assertEquals(partialDate, withDate.partialDate) + } + + + @Test + fun testFindAndRemoveLabel_NoLabel() { + val c = ContactReader(VCard()) + assertNull(c.findAndRemoveLabel("item1")) + } + + @Test + fun testFindAndRemoveLabel_Label() { + val vCard = VCard().apply { + addProperty(XAbLabel("Test Label").apply { group = "item1" }) + } + val c = ContactReader(vCard) + assertEquals("Test Label", vCard.getProperty(XAbLabel::class.java).value) + assertEquals("Test Label", c.findAndRemoveLabel("item1")) + assertNull(vCard.getProperty(XAbLabel::class.java)) + } + + @Test + fun testFindAndRemoveLabel_Label_Empty() { + val vCard = VCard().apply { + addProperty(XAbLabel("").apply { group = "item1" }) + } + val c = ContactReader(vCard) + assertEquals("", vCard.getProperty(XAbLabel::class.java).value) + assertNull(c.findAndRemoveLabel("item1")) + assertNull(vCard.getProperty(XAbLabel::class.java)) + } + + @Test + fun testFindAndRemoveLabel_LabelWithOtherGroup() { + val vCard = VCard().apply { + addProperty(XAbLabel("Test Label").apply { group = "item1" }) + } + val c = ContactReader(vCard) + assertEquals("Test Label", vCard.getProperty(XAbLabel::class.java).value) + assertNull(c.findAndRemoveLabel("item2")) + assertEquals("Test Label", vCard.getProperty(XAbLabel::class.java).value) + } + + + @Test + fun testGetPhotoBytes_Binary() { + val sample = ByteArray(128) + assertEquals(sample, ContactReader.fromVCard(VCard().apply { + addPhoto(Photo(sample, ImageType.JPEG)) + }).photo) + } + + @Test + fun testGetPhotoBytes_Downloader() { + val sample = ByteArray(128) + val sampleUrl = "http://example.com/photo.jpg" + val downloader = object: Contact.Downloader { + override fun download(url: String, accepts: String): ByteArray? { + return if (url == sampleUrl && accepts == "image/*") + sample + else + null + } + } + assertEquals(sample, ContactReader.fromVCard(VCard().apply { + addPhoto(Photo(sampleUrl, ImageType.JPEG)) + }, downloader).photo) + } + + + @Test + fun testUriToUid() { + assertEquals("uid", ContactReader.uriToUid("uid")) + assertEquals("urn:uid", ContactReader.uriToUid("urn:uid")) + assertEquals("12345", ContactReader.uriToUid("urn:uuid:12345")) + assertNull(ContactReader.uriToUid("")) + assertNull(ContactReader.uriToUid("urn:uuid:")) + } + +}
\ No newline at end of file diff --git a/src/test/java/at/bitfire/vcard4android/ContactTest.kt b/src/test/java/at/bitfire/vcard4android/ContactTest.kt index 75ffc35..f8aa19a 100644 --- a/src/test/java/at/bitfire/vcard4android/ContactTest.kt +++ b/src/test/java/at/bitfire/vcard4android/ContactTest.kt @@ -10,7 +10,7 @@ package at.bitfire.vcard4android import ezvcard.VCardVersion import ezvcard.parameter.* -import ezvcard.property.* +import ezvcard.property.Birthday import ezvcard.util.PartialDate import org.apache.commons.io.IOUtils import org.junit.Assert.* @@ -18,9 +18,10 @@ import org.junit.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStreamReader -import java.io.StringReader import java.nio.charset.Charset import java.text.SimpleDateFormat +import java.time.ZoneId +import java.time.ZonedDateTime import java.util.* class ContactTest { @@ -32,175 +33,18 @@ class ContactTest { private fun regenerate(c: Contact, vCardVersion: VCardVersion): Contact { val os = ByteArrayOutputStream() - c.write(vCardVersion, GroupMethod.CATEGORIES, os) + c.writeVCard(vCardVersion, GroupMethod.CATEGORIES, os) return Contact.fromReader(InputStreamReader(ByteArrayInputStream(os.toByteArray()), Charsets.UTF_8), null).first() } private fun toString(c: Contact, groupMethod: GroupMethod, vCardVersion: VCardVersion): String { val os = ByteArrayOutputStream() - c.write(vCardVersion, groupMethod, os) + c.writeVCard(vCardVersion, groupMethod, os) return os.toString() } @Test - fun testDropEmptyProperties() { - val vcard = "BEGIN:VCARD\n" + - "VERSION:4.0\n" + - "FN:Sample with empty values\n" + - "TEL:12345\n" + - "TEL:\n" + - "EMAIL:test@example.com\n" + - "EMAIL:\n" + - "END:VCARD" - val c = Contact.fromReader(StringReader(vcard), null).first() - assertEquals(1, c.phoneNumbers.size) - assertEquals("12345", c.phoneNumbers.first.property.text) - assertEquals(1, c.emails.size) - assertEquals("test@example.com", c.emails.first.property.value) - } - - @Test - fun testGenerateOrganizationOnly() { - val c = Contact() - c.uid = UUID.randomUUID().toString() - val org = Organization() - org.values.add("My Organization") - org.values.add("My Department") - c.organization = org - - // vCard 3 needs FN and N - var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) - assertTrue(vCard.contains("\nORG:My Organization;My Department\r\n")) - assertTrue(vCard.contains("\nFN:My Organization\r\n")) - assertTrue(vCard.contains("\nN:\r\n")) - - // vCard 4 only needs FN - vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) - assertTrue(vCard.contains("\nORG:My Organization;My Department\r\n")) - assertTrue(vCard.contains("\nFN:My Organization\r\n")) - assertFalse(vCard.contains("\nN:")) - } - - @Test - fun testGenerateOrgDepartmentOnly() { - val c = Contact() - c.uid = UUID.randomUUID().toString() - val org = Organization() - org.values.add("") - org.values.add("My Department") - c.organization = org - - // vCard 3 needs FN and N - var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) - assertTrue(vCard.contains("\nORG:;My Department\r\n")) - assertTrue(vCard.contains("\nFN:My Department\r\n")) - assertTrue(vCard.contains("\nN:\r\n")) - - // vCard 4 only needs FN - vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) - assertTrue(vCard.contains("\nORG:;My Department\r\n")) - assertTrue(vCard.contains("\nFN:My Department\r\n")) - assertFalse(vCard.contains("\nN:")) - } - - @Test - fun testGenerateGroup() { - val c = Contact() - c.uid = UUID.randomUUID().toString() - c.displayName = "My Group" - c.group = true - c.members += "member1" - c.members += "member2" - - // vCard 3 needs FN and N - // exception for Apple: "N:<group name>" - var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) - assertTrue(vCard.contains("\nX-ADDRESSBOOKSERVER-KIND:group\r\n")) - assertTrue(vCard.contains("\nFN:My Group\r\n")) - assertTrue(vCard.contains("\nN:My Group\r\n")) - assertTrue(vCard.contains("\nX-ADDRESSBOOKSERVER-MEMBER:urn:uuid:member1\r\n")) - assertTrue(vCard.contains("\nX-ADDRESSBOOKSERVER-MEMBER:urn:uuid:member2\r\n")) - - // vCard 4 only needs FN - vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) - assertTrue(vCard.contains("\nKIND:group\r\n")) - assertTrue(vCard.contains("\nFN:My Group\r\n")) - assertFalse(vCard.contains("\nN:")) - assertTrue(vCard.contains("\nMEMBER:urn:uuid:member1\r\n")) - assertTrue(vCard.contains("\nMEMBER:urn:uuid:member2\r\n")) - } - - @Test - fun testGenerateWithoutName() { - /* no data */ - val c = Contact() - // vCard 3 needs FN and N - var vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) - assertTrue(vCard.contains("\nFN:\r\n")) - assertTrue(vCard.contains("\nN:\r\n")) - // vCard 4 only needs FN - vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) - assertTrue(vCard.contains("\nFN:\r\n")) - assertFalse(vCard.contains("\nN:")) - - /* only UID */ - c.uid = UUID.randomUUID().toString() - vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) - // vCard 3 needs FN and N - assertTrue(vCard.contains("\nFN:${c.uid}\r\n")) - assertTrue(vCard.contains("\nN:\r\n")) - // vCard 4 only needs FN - vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) - assertTrue(vCard.contains("\nFN:${c.uid}\r\n")) - assertFalse(vCard.contains("\nN:")) - - // phone number available - c.phoneNumbers += LabeledProperty(Telephone("12345")) - assertTrue(toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0).contains("\nFN:12345\r\n")) - - // email address available - c.emails += LabeledProperty(Email("test@example.com")) - assertTrue(toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0).contains("\nFN:test@example.com\r\n")) - - // nick name available - c.nickName = LabeledProperty(Nickname().apply { - values += "Nikki" - }) - assertTrue(toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0).contains("\nFN:Nikki\r\n")) - } - - @Test - fun testGenerateLabeledProperty() { - var c = Contact() - c.uid = UUID.randomUUID().toString() - c.phoneNumbers += LabeledProperty(Telephone("12345"), "My Phone") - val vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) - assertTrue(vCard.contains("\ngroup1.TEL:12345\r\n")) - assertTrue(vCard.contains("\ngroup1.X-ABLabel:My Phone\r\n")) - - c = regenerate(c, VCardVersion.V4_0) - assertEquals("12345", c.phoneNumbers.first.property.text) - assertEquals("My Phone", c.phoneNumbers.first.label) - } - - @Test - fun testInvalidREV() { - val c = parseContact("invalid-rev.vcf") - assertFalse(c.unknownProperties!!.contains("REV")) - } - - @Test - fun testUnknownPropertiesLabels() { - // X-ABLabels belonging to unknown properties shouldn't be dropped - val c = parseContact("unknown-properties-with-labels.vcf") - assertEquals("Unknown property with label", c.displayName) - assertTrue(c.unknownProperties!!.contains("item1.X-Unknown:TestValue")) - assertTrue(c.unknownProperties!!.contains("item1.X-ABLabel:TestLabel")) - } - - - @Test fun testVCard3FieldsAsVCard3() { val c = regenerate(parseContact("allfields-vcard3.vcf"), VCardVersion.V3_0) @@ -312,7 +156,6 @@ class ContactTest { assertEquals("Klöster-Reich", addr.property.country) assertEquals("BEGIN:VCARD\r\n" + "VERSION:3.0\r\n" + - "PRODID:ez-vcard 0.11.2\r\n" + "X-TEST;A=B:Value\r\n" + "END:VCARD\r\n", c.unknownProperties) @@ -330,7 +173,7 @@ class ContactTest { var url1 = false var url2 = false for (url in c.urls) { - if ("https://davdroid.bitfire.at/" == url.property.value && url.property.type == null && url.label == null) + if ("https://www.davx5.com/" == url.property.value && url.property.type == null && url.label == null) url1 = true if ("http://www.swbyps.restaurant.french/~chezchic.html" == url.property.value && "x-blog" == url.property.type && "blog" == url.label) url2 = true @@ -342,6 +185,12 @@ class ContactTest { assertEquals("1996-04-15", dateFormat.format(c.birthDay!!.date)) // ANNIVERSARY assertEquals("2014-08-12", dateFormat.format(c.anniversary!!.date)) + // X-ABDATE + assertEquals(1, c.customDates.size) + c.customDates.first.also { date -> + assertEquals("Custom Date", date.label) + assertEquals(ZonedDateTime.of(2021, 7, 29, 0, 0, 0, 0, ZoneId.systemDefault()).toInstant(), date.property.date.toInstant()) + } // RELATED assertEquals(2, c.relations.size) @@ -351,7 +200,7 @@ class ContactTest { assertEquals("Ägidius", rel.text) rel = c.relations[1] assertTrue(rel.types.contains(RelatedType.PARENT)) - assertEquals("muuuum", rel.text) + assertEquals("muuum@example.com", rel.uri) // PHOTO javaClass.classLoader!!.getResourceAsStream("lol.jpg").use { photo -> diff --git a/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt new file mode 100644 index 0000000..bab087b --- /dev/null +++ b/src/test/java/at/bitfire/vcard4android/ContactWriterTest.kt @@ -0,0 +1,492 @@ +package at.bitfire.vcard4android + +import at.bitfire.vcard4android.property.* +import ezvcard.VCard +import ezvcard.VCardDataType +import ezvcard.VCardVersion +import ezvcard.parameter.ImageType +import ezvcard.property.* +import ezvcard.util.PartialDate +import org.junit.Assert.* +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.net.URI +import java.time.ZoneOffset +import java.util.* + +class ContactWriterTest { + + // test specific fields + + @Test + fun testAddress() { + val address = Address().apply { + streetAddress = "Test Street" + country = "XX" + } + val vCard = generate { + addresses.add(LabeledProperty(address)) + } + assertEquals(address, vCard.addresses.first()) + } + + + @Test + fun testAnniversary_vCard3() { + val date = Date(121, 6, 30) + val vCard = generate(version = VCardVersion.V3_0) { + anniversary = Anniversary(date) + } + assertNull(vCard.anniversary) + assertEquals(date, vCard.getProperty(XAbDate::class.java).date) + } + + @Test + fun testAnniversary_vCard4() { + val ann = Anniversary(Date(121, 6, 30)) + val vCard = generate(version = VCardVersion.V4_0) { + anniversary = ann + } + assertEquals(ann, vCard.anniversary) + } + + + @Test + fun testBirthday() { + val bday = Birthday(Date(121, 6, 30)) + val vCard = generate { + birthDay = bday + } + assertEquals(bday, vCard.birthday) + } + + + @Test + fun testCustomDate() { + val date = XAbDate(Date(121, 6, 30)) + val vCard = generate { + customDates += LabeledProperty(date) + } + assertEquals(date, vCard.getProperty(XAbDate::class.java)) + } + + + @Test + fun testCategories_Some() { + val vCard = generate { + categories += "cat1" + categories += "cat2" + } + assertEquals("cat1", vCard.categories.values[0]) + assertEquals("cat2", vCard.categories.values[1]) + } + + @Test + fun testCategories_None() { + val vCard = generate { } + assertNull(vCard.categories) + } + + + @Test + fun testEmail() { + val vCard = generate { + emails.add(LabeledProperty(Email("test@example.com"))) + } + assertEquals("test@example.com", vCard.emails.first().value) + } + + + @Test + fun testFn_vCard3_NoFn_Organization() { + val vCard = generate(version = VCardVersion.V3_0) { + organization = Organization().apply { + values.add("org") + values.add("dept") + } + // other values should be ignored because organization is available + nickName = LabeledProperty(Nickname().apply { + values.add("nick1") + }) + } + assertEquals("org / dept", vCard.formattedName.value) + } + + @Test + fun testFn_vCard3_NoFn_NickName() { + val vCard = generate(version = VCardVersion.V3_0) { + nickName = LabeledProperty(Nickname().apply { + values.add("nick1") + }) + // other values should be ignored because nickname is available + emails += LabeledProperty(Email("test@example.com")) + } + assertEquals("nick1", vCard.formattedName.value) + } + + @Test + fun testFn_vCard3_NoFn_Email() { + val vCard = generate(version = VCardVersion.V3_0) { + emails += LabeledProperty(Email("test@example.com")) + // other values should be ignored because email is available + phoneNumbers += LabeledProperty(Telephone("+1 555 12345")) + } + assertEquals("test@example.com", vCard.formattedName.value) + } + + @Test + fun testFn_vCard3_NoFn_Phone() { + val vCard = generate(version = VCardVersion.V3_0) { + phoneNumbers += LabeledProperty(Telephone("+1 555 12345")) + // other values should be ignored because phone is available + uid = "uid" + } + assertEquals("+1 555 12345", vCard.formattedName.value) + } + + @Test + fun testFn_vCard3_NoFn_Uid() { + val vCard = generate(version = VCardVersion.V3_0) { + uid = "uid" + } + assertEquals("uid", vCard.formattedName.value) + } + + @Test + fun testFn_vCard3_NoFn_Nothing() { + val vCard = generate(version = VCardVersion.V3_0) { } + assertEquals("", vCard.formattedName.value) + } + + @Test + fun testFn_vCard3_Fn() { + val vCard = generate(version = VCardVersion.V3_0) { + displayName = "Display Name" + } + assertEquals("Display Name", vCard.formattedName.value) + } + + @Test + fun testFn_vCard4_NoFn() { + val vCard = generate(version = VCardVersion.V4_0) { } + assertNull(vCard.formattedName) + } + + @Test + fun testFn_vCard4_Fn() { + val vCard = generate(version = VCardVersion.V4_0) { + displayName = "Display Name" + } + assertEquals("Display Name", vCard.formattedName.value) + } + + + @Test + fun testImpp() { + val vCard = generate { + impps.add(LabeledProperty(Impp.xmpp("test@example.com"))) + } + assertEquals(URI("xmpp:test@example.com"), vCard.impps.first().uri) + } + + + @Test + fun testKindAndMember_vCard3() { + val vCard = generate(GroupMethod.GROUP_VCARDS, VCardVersion.V3_0) { + group = true + members += "member1" + } + assertEquals(Kind.GROUP, vCard.getProperty(XAddressBookServerKind::class.java).value) + assertEquals("urn:uuid:member1", vCard.getProperty(XAddressBookServerMember::class.java).value) + } + + @Test + fun testKindAndMember_vCard4() { + val vCard = generate(GroupMethod.GROUP_VCARDS, VCardVersion.V4_0) { + group = true + members += "member1" + } + assertEquals(Kind.GROUP, vCard.getProperty(Kind::class.java).value) + assertEquals("urn:uuid:member1", vCard.members.first().value) + } + + + @Test + fun testN_vCard3_NoN() { + val vCard = generate(version = VCardVersion.V3_0) { } + assertEquals(StructuredName(), vCard.structuredName) + } + + @Test + fun testN_vCard4_NoN() { + val vCard = generate(version = VCardVersion.V4_0) { } + assertNull(vCard.structuredName) + } + + @Test + fun testN() { + val vCard = generate(version = VCardVersion.V4_0) { + prefix = "P1. P2." + givenName = "Given" + middleName = "Middle1 Middle2" + familyName = "Family" + suffix = "S1 S2" + } + assertEquals(StructuredName().apply { + prefixes += "P1." + prefixes += "P2." + given = "Given" + additionalNames += "Middle1" + additionalNames += "Middle2" + family = "Family" + suffixes += "S1" + suffixes += "S2" + }, vCard.structuredName) + } + + + @Test + fun testNote() { + val vCard = generate { note = "Some Note" } + assertEquals("Some Note", vCard.notes.first().value) + } + + + @Test + fun testOrganization() { + val org = Organization().apply { + values.add("Org") + values.add("Dept") + } + val vCard = generate { + organization = org + jobTitle = "CEO" + jobDescription = "Executive" + } + assertEquals(org, vCard.organization) + assertEquals("CEO", vCard.titles.first().value) + assertEquals("Executive", vCard.roles.first().value) + } + + + @Test + fun testPhoto() { + val testPhoto = ByteArray(128) + val vCard = generate { photo = testPhoto } + assertEquals(Photo(testPhoto, ImageType.JPEG), vCard.photos.first()) + } + + + @Test + fun testRelation() { + val rel = Related.email("bigbrother@example.com") + val vCard = generate { relations += rel } + assertEquals(rel, vCard.relations.first()) + } + + + @Test + fun testTel() { + val vCard = generate { + phoneNumbers.add(LabeledProperty(Telephone("+1 555 12345"))) + } + assertEquals("+1 555 12345", vCard.telephoneNumbers.first().text) + } + + + @Test + fun testUid() { + val vCard = generate { uid = "12345" } + assertEquals("12345", vCard.uid.value) + } + + + @Test + fun testUnknownProperty() { + val vCard = generate { + unknownProperties = "BEGIN:VCARD\r\n" + + "FUTURE-PROPERTY;X-TEST=test1;TYPE=uri:12345\r\n" + + "END:VCARD\r\n" + } + assertEquals("12345", vCard.getExtendedProperty("FUTURE-PROPERTY").value) + assertEquals("test1", vCard.getExtendedProperty("FUTURE-PROPERTY").getParameter("X-TEST")) + } + + + @Test + fun testUrl() { + val vCard = generate { urls += LabeledProperty(Url("https://example.com")) } + assertEquals("https://example.com", vCard.urls.first().value) + } + + + @Test + fun testXPhoneticName() { + val vCard = generate() { + phoneticGivenName = "Given" + phoneticMiddleName = "Middle" + phoneticFamilyName = "Family" + } + assertEquals("Given", vCard.getProperty(XPhoneticFirstName::class.java).value) + assertEquals("Middle", vCard.getProperty(XPhoneticMiddleName::class.java).value) + assertEquals("Family", vCard.getProperty(XPhoneticLastName::class.java).value) + } + + + + // test generator helpers + + @Test + fun testAddLabeledProperty_NoLabel() { + val vCard = generate { + nickName = LabeledProperty(Nickname().apply { + values.add("nick1") + }) + } + assertEquals(2 /* NICK + REV */, vCard.properties.size) + assertEquals("nick1", vCard.nickname.values.first()) + } + + @Test + fun testAddLabeledProperty_Label() { + val vCard = generate { + nickName = LabeledProperty(Nickname().apply { + values.add("nick1") + }, "label1") + } + assertEquals(3 /* NICK + X-ABLABEL + REV */, vCard.properties.size) + vCard.nickname.apply { + assertEquals("nick1", values.first()) + assertEquals("item1", group) + } + vCard.getProperty(XAbLabel::class.java).apply { + assertEquals("label1", value) + assertEquals("item1", group) + } + } + + @Test + fun testAddLabeledProperty_Label_CollisionWithUnknownProperty() { + val vCard = generate { + unknownProperties = "BEGIN:VCARD\n" + + "item1.X-TEST:This property is blocking the first item ID\n" + + "END:VCARD" + nickName = LabeledProperty(Nickname().apply { + values.add("nick1") + }, "label1") + } + assertEquals(4 /* X-TEST + NICK + X-ABLABEL + REV */, vCard.properties.size) + vCard.nickname.apply { + assertEquals("nick1", values.first()) + assertEquals("item2", group) + } + vCard.getProperty(XAbLabel::class.java).apply { + assertEquals("label1", value) + assertEquals("item2", group) + } + } + + + @Test + fun testRewritePartialDate_vCard3_Date() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS) + val date = Birthday(Date(121, 6, 30)) + generator.rewritePartialDate(date) + assertEquals(Date(121, 6, 30), date.date) + assertNull(date.partialDate) + } + + @Test + fun testRewritePartialDate_vCard4_Date() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val date = Birthday(Date(121, 6, 30)) + generator.rewritePartialDate(date) + assertEquals(Date(121, 6, 30), date.date) + assertNull(date.partialDate) + assertEquals(0, date.parameters.size()) + } + + @Test + fun testRewritePartialDate_vCard3_PartialDateWithYear() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS) + val date = Birthday(PartialDate.parse("20210730")) + generator.rewritePartialDate(date) + assertEquals(Date(121, 6, 30), date.date) + assertNull(date.partialDate) + assertEquals(0, date.parameters.size()) + } + + @Test + fun testRewritePartialDate_vCard4_PartialDateWithYear() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val date = Birthday(PartialDate.parse("20210730")) + generator.rewritePartialDate(date) + assertNull(date.date) + assertEquals(PartialDate.parse("20210730"), date.partialDate) + assertEquals(0, date.parameters.size()) + } + + @Test + fun testRewritePartialDate_vCard3_PartialDateWithoutYear() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V3_0, GroupMethod.GROUP_VCARDS) + val date = Birthday(PartialDate.parse("--0730")) + generator.rewritePartialDate(date) + assertEquals(Date(-300+4, 6, 30), date.date) + assertNull(date.partialDate) + assertEquals(1, date.parameters.size()) + assertEquals("1604", date.getParameter(Contact.DATE_PARAMETER_OMIT_YEAR)) + } + + @Test + fun testRewritePartialDate_vCard4_PartialDateWithoutYear() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + val date = Birthday(PartialDate.parse("--0730")) + generator.rewritePartialDate(date) + assertNull(date.date) + assertEquals(PartialDate.parse("--0730"), date.partialDate) + assertEquals(0, date.parameters.size()) + } + + + @Test + fun testWriteVCard() { + val generator = ContactWriter.fromContact(Contact(), VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + generator.vCard.revision = Revision(Calendar.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC.id)).apply { + set(2021, 6, 30, 1, 2, 3) + }) + + val stream = ByteArrayOutputStream() + generator.writeVCard(stream) + assertEquals("BEGIN:VCARD\r\n" + + "VERSION:4.0\r\n" + + "PRODID:ez-vcard 0.11.2\r\n" + + "REV:20210730T010203Z\r\n" + + "END:VCARD\r\n", stream.toString()) + } + + @Test + fun testWriteVCard_CaretEncoding() { + val stream = ByteArrayOutputStream() + val contact = Contact().apply { + addresses += LabeledProperty(Address().apply { + label = "Li^ne 1,1 - \" -" + streetAddress = "Line1" + country = "Line2" + }) + } + ContactWriter + .fromContact(contact, VCardVersion.V4_0, GroupMethod.GROUP_VCARDS) + .writeVCard(stream) + assertTrue(stream.toString().contains("ADR;LABEL=\"Li^^ne 1,1 - ^' -\":;;Line1;;;;Line2")) + } + + + // helpers + + private fun generate(groupMethod: GroupMethod = GroupMethod.GROUP_VCARDS, version: VCardVersion = VCardVersion.V4_0, prepare: Contact.() -> Unit): VCard { + val contact = Contact() + contact.run(prepare) + return ContactWriter.fromContact(contact, version, groupMethod).vCard + } + +}
\ No newline at end of file diff --git a/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt b/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt index 09b7528..ae10388 100644 --- a/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt +++ b/src/test/java/at/bitfire/vcard4android/EzVCardTest.kt @@ -9,6 +9,9 @@ package at.bitfire.vcard4android import ezvcard.Ezvcard +import ezvcard.VCard +import ezvcard.VCardVersion +import ezvcard.property.Address import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Before @@ -62,4 +65,20 @@ class EzVCardTest { assertNotNull(vCard.revision) } + @Test + fun testGenerateCaretNewline() { + val vCard = VCard() + vCard.addAddress(Address().apply { + label = "Li^ne 1,1\n- \" -" + streetAddress = "Line 1" + country = "Line 2" + }) + val str = Ezvcard .write(vCard) + .version(VCardVersion.V4_0) + .caretEncoding(true) + .go().lines().filter { it.startsWith("ADR") }.first() + //assertEquals("ADR;LABEL=\"Li^^ne 1,1^n- ^' -\":;;Line 1;;;;Line 2", str) + assertEquals("ADR;LABEL=\"Li^^ne 1,1\\n- ^' -\":;;Line 1;;;;Line 2", str) + } + } diff --git a/src/test/resources/allfields-vcard3.vcf b/src/test/resources/allfields-vcard3.vcf index af253a5..5a2a329 100644 --- a/src/test/resources/allfields-vcard3.vcf +++ b/src/test/resources/allfields-vcard3.vcf @@ -29,13 +29,15 @@ NOTE:This fax number is operational 0800 to 1715 EST\, Mon-Fri. NOTE:Second note CATEGORIES:A,B\'C -URL:https://davdroid.bitfire.at/ +URL:https://www.davx5.com/ blog.URL;TYPE=x-blog:http://www.swbyps.restaurant.french/~chezchic.html blog.X-ABLabel:blog BDAY:1996-04-15 ANNIVERSARY:2014-08-12 RELATED;CHARSET=UTF-8;TYPE=co-worker,crush;VALUE=text:Ägidius -RELATED;TYPE=parent;VALUE=text:muuuum +RELATED;TYPE=parent;VALUE=uri:muuum@example.com PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRgABAQIAHAAcAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAKACABAREA/8QAFgABAQEAAAAAAAAAAAAAAAAAAAgE/8QAIBAAAQQCAwEBAQAAAAAAAAAABAECAwUABgcREggUFf/aAAgBAQAAPwC/huVQw/pq74hvt0qhP16fQ2mu0hBEERRhUhV0h0g7V6ln6hDFV6J6axrEd0305XS/xR9SbWRxtLudn9HibSOLT8SW2wWBf8VsGvl3F7KPehyOFHiZAxozGorZ/UsKKrvbV6cl1VNtVX9UHe0VmJY1tiPGWGYJM2aAmCRqOjljkaqtexzVRyORVRUVFTNeMYxn/9k= X-TEST;A=B:Value +item2.X-ABDate:20210729 +item2.X-ABLabel:Custom Date END:VCARD diff --git a/src/test/resources/invalid-rev.vcf b/src/test/resources/invalid-rev.vcf deleted file mode 100644 index c89bdd9..0000000 --- a/src/test/resources/invalid-rev.vcf +++ /dev/null @@ -1,21 +0,0 @@ -BEGIN:VCARD -VERSION:4.0 -PRODID:+//IDN bitfire.at//DAVdroid/1.11.4.1-ose ez-vcard/0.10.4 -X-EVOLUTION-FILE-AS:Xxxxxx-xxxx\, Loïc -X-EVOLUTION-MANAGER: -X-EVOLUTION-ASSISTANT: -X-EVOLUTION-SPOUSE: -X-EVOLUTION-BLOG-URL: -X-EVOLUTION-VIDEO-URL: -X-MOZILLA-HTML:FALSE -REV;VALUE=timestamp:2018-04-30T07:42:25Z(1) -CALURI: -FBURL: -UID:pas-id-0c6bbc36b403f6e885c75e4835f796bba6fcdc12 -FN:Loïc Xxxxxx-xxxx -N:Xxxxxx-xxxx;Loïc;;; -ORG:xxxxxxxmail -EMAIL;TYPE=work:loic.Xxxxxx-xxxx@xxxxxxxmail.com -URL: -REV:20180619T131622Z -END:VCARD |