mangstadt / ez-vcard

A vCard parser library for Java
Other
398 stars 92 forks source link

Can't call VCardPropertyScribe.write* explicitly for all properties #119

Closed rfc2822 closed 2 years ago

rfc2822 commented 2 years ago

Hi,

I want to serialize some VCardPropertys because they're unknown on an application level and should later be inserted when creating the vCard again. In the FAQ, I can find:

VCard vcard = ...
ScribeIndex index = new ScribeIndex();
WriteContext context = new WriteContext(VCardVersion.V3_0, null, true);
for (VCardProperty property : vcard) {
  VCardPropertyScribe scribe = index.getPropertyScribe(property);
  String name = scribe.getPropertyName();
  String value = scribe.writeText(property, context);
  System.out.println(name + " = " + value);
}

However, this doesn't work because property is of type VCardProperty and scribe is then of type VCardPropertyScribe<out VCardProperty> (Java: VCardPropertyScribe<? extends VCardProperty>).

However, property doesn't necessarily extend VCardProperty, but is a VCardProperty. So the Kotlin compiler changes the signature of the examples's scribe.writeText to writeText(property: Nothing!):

val vCard: VCard
val scribeIndex = ScribeIndex()
for (prop in vCard.properties) {
    // prop is now of type VCardProperty

    val scribe = scribeIndex.getPropertyScribe(prop)
    // scribe is of type VCardPropertyScribe<out VCardProperty>
    // (Java: VCardPropertyScribe<? extends VCardProperty>)

    scribe.writeJson(prop)
    // doesn't work because the signature of scribe.writeJson is
    // scribe.writeJson(property: Nothing!)

    // I think this is because prop is a VCardProperty, and not necessarily some subclass of VCardProperty.
}

Is that intended behavior or some Java/Kotlin incompatibility? As I understand it, the problem should occur for plain Java, too.

How is that intended to be used?

mangstadt commented 2 years ago

Yes, the problem does occur in plain Java! Thank you for pointing that out.

That code sample from the FAQ only works if you leave out the generics and ignore the type safety warnings.

In fact, similar code is used by the writer classes (VCardWriter, JCardWriter, etc). Generics are left out of the VCardPropertyScribe scribe variable declaration. A method-level annotation is used to suppress the warnings.

@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException {
  //...
  for (VCardProperty property : propertiesToAdd) {
    VCardPropertyScribe scribe = index.getPropertyScribe(property);
    String value = scribe.writeText(property, context);
    //...
  }
 //...
}

I understand why the compiler has a problem with this. The compiler has no way of knowing that the VCardPropertyScribe implementation correctly corresponds with the given VCardProperty implementation. For example, in the code below, a FormattedName property object is passed into a AddressPropertyScribe object. The compiler gives several warnings, but allows the code to be compiled and run. When the code is run, the last line throws a ClassCastException because it is trying to cast the FormattedName object to an Address object.

ScribeIndex index = new ScribeIndex();
WriteContext context = new WriteContext(VCardVersion.V3_0, null, true);
VCardProperty property = new FormattedName("value");
VCardPropertyScribe scribe = index.getPropertyScribe(Address.class); //compiler: raw type warning
String value = scribe.writeText(property, context); //compiler: type safety warning; ClassCastException thrown at runtime

ez-vcard never throws this exception because the code is written such that the property object that is passed into the index.getPropertyScribe method is ALSO passed into the scribe.writeText method. It's never a different object, as it is in the failing code sample above.

One potential fix is to change the method signature of VCardPropertyScribe.writeText to accept a VCardProperty object instead of a <T extends VCardProperty> object. However, doing that prevents the scribe classes from being able to use their corresponding property classes in each of their "write" methods (e.g. using the Address class in the AddressPropertyScribe.writeText method signature.

At the moment, I am not sure what else can be done to resolve this. I am open to ideas of course!

rfc2822 commented 2 years ago

Thanks for your explanations! I have found how to do the same unchecked conversion with Kotlin:


val vCard: VCard
val scribeIndex = ScribeIndex()
for (prop in vCard.properties) {
    // prop is now of type VCardProperty

    @Suppress("UNCHECKED_CAST")
    val scribe = scribeIndex.getPropertyScribe(prop) as VCardPropertyScribe<VCardProperty>
    // scribe is now of type VCardPropertyScribe<VCardProperty> 

    scribe.writeJson(prop)       // works!
}