johncarl81 / parceler

:package: Android Parcelables made easy through code generation.
http://parceler.org
Apache License 2.0
3.56k stars 273 forks source link

Polymorphic deserialization of list items #249

Open WonderCsabo opened 7 years ago

WonderCsabo commented 7 years ago

I have the following structure:

@Parcel
public class Zoo {
  List<Animal> animals;
  List<Tiger> tigers;
}
@Parcel
public class Animal {

}
@Parcel
public class Tiger  extends Animal {

}

I put objects of different Animal subclasses at runtime. When i deserialize the parcel, i only get back Animal objects, and not Parrot, Tiger etc. I tried to implementations field of the Parcel annotations on Animal, but it fails with the following exception:

Caused by: org.parceler.transfuse.util.TransfuseRuntimeException: Unable to perform code generation
        at org.parceler.transfuse.transaction.CodeGenerationScopedTransactionWorker.innerRun(CodeGenerationScopedTransactionWorker.java:53)
        at org.parceler.transfuse.transaction.AbstractCompletionTransactionWorker.run(AbstractCompletionTransactionWorker.java:35)
        at org.parceler.transfuse.transaction.ScopedTransactionWorker.run(ScopedTransactionWorker.java:55)
        at org.parceler.transfuse.transaction.Transaction.run(Transaction.java:77)
        at org.parceler.guava.util.concurrent.MoreExecutors$DirectExecutorService.execute(MoreExecutors.java:299)
        at org.parceler.transfuse.transaction.TransactionProcessorPool.execute(TransactionProcessorPool.java:60)
        at org.parceler.transfuse.transaction.TransactionProcessorComposite.execute(TransactionProcessorComposite.java:37)
        at org.parceler.transfuse.transaction.TransactionProcessorChain.execute(TransactionProcessorChain.java:38)
        at org.parceler.internal.ParcelProcessor.execute(ParcelProcessor.java:83)
        at org.parceler.ParcelAnnotationProcessor.process(ParcelAnnotationProcessor.java:84)
        at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
        at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
        at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
        at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
        at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
        at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
        at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:856)
        at com.sun.tools.javac.main.Main.compile(Main.java:523)
        ... 88 more
Caused by: javax.annotation.processing.FilerException: Attempt to recreate a file for type `zoo.Tiger`$$Parcelable
        at com.sun.tools.javac.processing.JavacFiler.checkNameAndExistence(JavacFiler.java:522)
        at com.sun.tools.javac.processing.JavacFiler.createSourceOrClassFile(JavacFiler.java:396)
        at com.sun.tools.javac.processing.JavacFiler.createSourceFile(JavacFiler.java:378)
        at org.parceler.transfuse.gen.FilerSourceCodeWriter.openBinary(FilerSourceCodeWriter.java:48)
        at org.parceler.codemodel.CodeWriter.openSource(CodeWriter.java:100)
        at org.parceler.codemodel.JPackage.createJavaSourceFileWriter(JPackage.java:486)
        at org.parceler.codemodel.JPackage.build(JPackage.java:437)
        at org.parceler.codemodel.JCodeModel.build(JCodeModel.java:311)
        at org.parceler.transfuse.transaction.CodeGenerationScopedTransactionWorker.innerRun(CodeGenerationScopedTransactionWorker.java:49)
        ... 105 more

If i remove the @Parcel annotation from Tiger, i got this

Parceler: Unable to find read/write generator for type java.util.List<zoo.Tiger> for zoo.tigers
johncarl81 commented 7 years ago

What version are you using @WonderCsabo?

WonderCsabo commented 7 years ago

I forgot to mention that: 1.1.6 .

johncarl81 commented 7 years ago

Hmm, looks like I need to lock down the implementations field a bit more.

By default, Parceler doesn't perform a runtime class check on the contents of your list. It will treat it as the declared type.

There is a way around this, however. You could use a converter that uses the Parcels.wrap/unwrap to perform the runtime check. It is a bit slower, but probably not noticeable:

@Parcel
public class Zoo {
  @ParcelPropertyConverter(ListParcelsWrapConverter.class)
  List<Animal> animals;
  @ParcelPropertyConverter(ListParcelsWrapConverter.class)
  List<Tiger> tigers;
}

public class ListParcelsWrapConverter extends ArrayListParcelConverter<Object> {

    @Override
    public void itemToParcel(Object input, Parcel parcel) {
        parcel.writeParcelable(Parcels.wrap(input), 0);
    }

    @Override
    public Object itemFromParcel(Parcel parcel) {
        return Parcels.unwrap(parcel.readParcelable(ListParcelsWrapConverter.class.getClassLoader()));
    }
}

We touch on this in the docs: http://parceler.org/#polymorphism and http://parceler.org/#custom_serialization

I am always open to ideas here if you have a better technique or thoughts.

WonderCsabo commented 7 years ago

Ehh, i read README file, but i did not know you have a fancy webpage. Thanks!

This is an acceptable workaround, thanks! I am not sure if it is feasible, but i think a new annotation, or a parameter to @Parcel would be uesful to mark the class as polymorphic. I think most users would want the concrete objects back.

WonderCsabo commented 7 years ago

Eh, i was too happy. This general class will not work.

I got this compilation error:

Zoo$$Parcelable.java
Error:(53, 107) error: incompatible types: List<Animal> cannot be converted to Collection<Object>
Error:(80, 119) error: incompatible types: ArrayList<Object> cannot be converted to List<Animal>
Zoo$$Parcelable.java
Error:(53, 109) error: incompatible types: List<Tiger> cannot be converted to Collection<Object>
Error:(80, 121) error: incompatible types: ArrayList<Object> cannot be converted to List<Tiger>

The obvious fix is changing to ListParcelsWrapConverter extends ArrayListParcelConverter<Animal>. But i also have to create a different class for ListParcelsWrapConverter extends ArrayListParcelConverter<Tiger>. 😢

johncarl81 commented 7 years ago

Ugh, sorry, I was too quick in giving an example... yeah, you're right you'd have to create a different converter for each type. I need to fix the converter validation and casting to the given type.

WonderCsabo commented 7 years ago

Cool, I created the two classes. Actually I could create a generic base class which provides the method implementations, and the subclasses only differ in the class signature (generic argument). Maybe you can provide the base class in the library if it is a common problem.

johncarl81 commented 7 years ago

One idea brought up before was to add an annotation property to trigger Parceler to use the Parcels.wrap/unwrap for polymetric behavior instead of having to deal with a converter. Something like this:

@Parcel
public class Zoo {
  @ParcelPropertyConverter(polymetric = true)
  List<Animal> animals;
}

I do want to keep away from using too much reflection (.getClass() in particular) as it is a slower opreation.

Oh, and if you're impressed by the website, you might be interested in whats backing it. I've used Jekyll, Asciidoc, and Travis to generate the site directly from Github. Makes editing the site easy because its just a PR or push to master. In fact, you can edit the contents via github's web interface.
Here's the github repo:https://github.com/johncarl81/parceler-site And the base "JAQ" project that I help maintain: https://github.com/asciidoctor/jekyll-asciidoc-quickstart

WonderCsabo commented 7 years ago

Yep, I also mentioned the annotation, but not this way. It would be great! I think I'd you earn the user about reflection, it is ok. There is no way to work around polymorphism I guess.

Your documentation setup is indeed impressive! Thanks for the info.