Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Mapper/Reader/Writer of a Collection #10

Merged
merged 5 commits into from
Mar 27, 2019

Conversation

tedynaidenov
Copy link
Contributor

Current gwt-jackson-apt implementation lacks support for Mapper (or Reader/Writer) over Collection class such as Set or List.
Nearly all needed code is already there, so the changes are minimal. There are 2 sets of changes:

  • addressing ObjectMapperProcessor and MapperGenerator hierarchy. In the case when Mapper is declared over a collection i.e.
    @JSONMapper interface CollectionMapper extends ObjectMapper<List<BeanObject>> { }
    generated MapperImp code creates corresponding code (in newSerializer()/newDeserializer()) to create needed collection serializers/deserializers

  • addressing JsonRegistry and JSONRegistrationProcessor. JsonRegistry is extended to have a key of newly introduced class Type. The latter is used to model relations between generic class and its parameters. The JSONRegistrationProcessor is modified to create corresponding code to register appropriate keys.

@vegegoku vegegoku added this to the 1.0 milestone Nov 19, 2018
Copy link

@niloc132 niloc132 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks interesting, thanks for the patch! The tests are a big light, and I'd personally like to see a little more detail on what might end up in the registry at runtime, make sure that the kinds of pitfalls that happen to gwt-rpc don't happen here.

Can you provide some docs or more tests that show what is supposed to happen here with some complex cases?

public static void tearDownAfterClass() throws Exception {
}

@Before

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if empty/unused, these setup/teardown methods should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @niloc132! I will remove them.

.build();
}

private void processType(TypeMirror type, StringBuilder result, List<ClassName> classNames) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider making this return a CodeBlock or the like instead, so that you can assemble the code as objects isntead of just concating strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with that. I will refactor the code to CodeBlock & CodeBlock.Builder instead of string concatenation.


for (TypeMirror type: declaredType.getTypeArguments()) {
result.append(".typeParam(");
processType(type, result, classNames);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a visitor, wouldnt it make more sense to do type.accept(this, v);?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, I will take into account for the code refactoring!

@Override
protected Iterable<MethodSpec> getMapperMethods(Element element, Name beanName) {
return Stream.of(makeNewDeserializerMethod(element, beanName), makeNewSerializerMethod(element, beanName))
.collect(Collectors.toSet());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this (and the other toSet calls) have to use a Set, or might a List make more sense? Using this requires that the underlying MethodSpec be made into a string (possibly more than once) in the process of building the set to check that none of the MethodSpecs are duplicates of each other. Since we just return an Iterable, it looks like the calling code isn't concerned with this, and from a quick look at makeNewDeserializerMethod it doesn't look like it should generate duplicates, so why spend the extra work here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point here... It should be Collectors.toList(). The original code was (and for BeanMapperGenerator still is) using Collectors.toSet().

.addCode(
CodeBlock.builder()
.add("return ")
.add(new FieldDeserializersChainBuilder(getElementType(element)).getInstance(getElementType(element)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this is a CodeBlock it might not be safe to preface it with a return like this, it would be legal for getInstance to provide a multi-line statement.

(note that this is wrong in other places too in the existing codebase)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@niloc132 I got your point but is not an easy fix. What we need here is a notion for an expression i.e. "return [expression]", but it seems the javapoet does not have such metaphor.
Probably it is better to include such restriction into the contract for:
FieldDeserializersChainBuilder.getInstance(..)
and
FieldSerializersChainBuilder.getInstance(..)

i.e. the CodeBlock returned from getInstance() must be valid java expression.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and as you noted elsewhere, the original code was also making this mistake.

Valid Java expression is one option, along the same lines you could just make it a plain String (since this is what add(CodeBlock) will do anyway, this way without ambiguity of it being a code block), a third could be to actually make it return a valid Java codeblock - let it generate the return and ; too?

I'm leaning toward the last, but perhaps in another patch, it would be a bigger change.

@@ -106,11 +115,10 @@ private MethodSpec createGetMethod(String name, String mapName, Class<?> returnT

private FieldSpec createConstantMap(String name, Class<?> jsonType) {
ClassName mapType = ClassName.get(Map.class);
ParameterizedTypeName classOfWildcard = ParameterizedTypeName.get(ClassName.get(Class.class),
WildcardTypeName.subtypeOf(Object.class));
ClassName typeClassName = ClassName.get(Type.class);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given the ClassName.get("org.dominokit..." reference above, pick a convention on how you reference other types? generally prefer the string version to avoid classpath issues when the user runs the codegen, but in this case the jars might only work tightly coupled together, i'm not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will change it to string version to avoid possible conflicts.

@ivmarkov
Copy link

@niloc132 - thanks for looking into it! We update the pull request with your recommendations and will provide a bit more extensive tests by tomorrow.

The pull request is indeed a bit more extensive than what is advertised. Besides "just" allowing the user to request an ObjectMapper implementation for collections, it can generate code for a lot of additional Java types, like for example:

  • List<Set<List<MyBean>>>
  • MyGenericBean<InstantiatedWithThisParticularType>

As for "avoiding the common GWT-RPC pitfalls", I'm not sure what you mean...

  • If you mean what happens if the user requests an ObjectMapper for a generic type List<T>, where T is unbound or not completely bound, it will fail, simply because a serializer/deserializer for the unbound type "T" cannot be generated; maybe the topic here is more about a user-friendly error message, etc.
  • Ditto for e.g. List<Object> - This is as supported as - say - requesting an ObjectMapper for the Object type. While it might not fail, it will also not return a meaningful mapper, and - unlike GWT-RPC - it will not even try to generate de/serialization logic for all descendants of Object, simply because the actual runtime types of the concrete List members are not examined at all...
  • But then, isn't that how not just gwt-jackson-apt, but Jackson itself works? For deserialization at least, you are supposed to pass in the "most instantiated" type you know about at deserialization time... which in our case is the return type of the method, as the actual type is of course not available from the incoming JSON payload...

One thing we should probably look into is how to address Java types with wildcards (like List<? extends MyBean> or Collection<? super MyBean>). I don't think we really need those - not in the first iteration at least, so probably just detecting these and outputting an error message might be good enough.

@ivmarkov
Copy link

See also intendia-oss/autorest#6

@niloc132
Copy link

@ivmarkov and thank you for working on this! I'm very pleased to see someone like you take an interest, and got permission from the authors here before assisting in the review, since it isn't an easy problem to tackle.

I didnt run through all the code in my head, or start to build examples with it, hoping to get a little more clarity from you in tests or docs first. My concern was mostly the second point - I had understood your initial statement to suggest that a hierarchy of types would be supported. In "normal" jackson, this works easily when serializing (reflection...) but you have to provide @JsonSubTypes or your own mapper to deserialize, and I wasn't sure if this patch was covering any or all of those cases.

Can you also talk a bit about the new Type class, and if it is part of the public api? if not, could it potentially just be replaced by string literals or int ids for brevity in compiled code?

@ivmarkov
Copy link

ivmarkov commented Nov 19, 2018

OK, I see where you are going. We are not as ambitious as to support @JsonSubTypes as of yet.

(Also, the state of gwt-jackson-apt as of now is such that it basically does not support neither @JsonSubTypes, nor - in fact - any other Jackson annotation yet. We had to implement support at least for @JsonIgnore, but that was trivial.)

We just wanted to start with a support for a simpler case - assuming your collections are homogeneous, you "ask" gwt-jackson-apt to generate an object mapper for a collection with the precise bean type you expect. In other words, no support for polymorphism so far. Needless to say, the precise bean type (and collection) needs to be reflected in the method return type as specified in the JAX-RS interface that we reuse on the client with autorest, and more over, you need some description of it at runtime passed to you by autorest, so that you can retrieve the proper object mapper from gwt-jackson-apt's registry (JsonRegistry). Hence btw our other pull request to autorest.

Hopefully this (and more) can be clarified with a few examples coming soon.

As for the Type class being non-public. It actually IS public, and I don't see a way around it. In our pull request, the Type class is the new key in gwt-jackson-apt's JsonRegistry instances. JSonRegistry used to be a map of object mappers keyed by Class, but that obviously does not work for parameterized types, hence Type came to being.

import org.dominokit.jacksonapt.processor.serialization.FieldSerializerChainBuilder;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unused import needs to be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will fix unused imports.


import javax.annotation.Generated;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will fix unused imports.

import org.dominokit.jacksonapt.ObjectMapper;
import org.dominokit.jacksonapt.ObjectReader;
import org.dominokit.jacksonapt.ObjectWriter;
import org.dominokit.jacksonapt.annotation.JSONRegistration;
import org.dominokit.jacksonapt.processor.AbstractMapperProcessor;
import org.dominokit.jacksonapt.registration.JsonRegistry;
import org.dominokit.jacksonapt.registration.Type;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused import

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I will fix unused imports.

@vegegoku
Copy link
Member

@tedynaidenov @tedynaidenov First i would like to thank you so much for this work, i do appreciate it.
One of the things i am struggling with, is finding time to document this lib, so the one last thing i am going to ask for this PR before i merge it is to add some docs - at least for the new feature - so it does not add more to what i have to document.

of course documenting any other parts is also very welcome .. Am i acting greedy here?! 😅

@tedynaidenov
Copy link
Contributor Author

@vegegoku , I will try to document this extension. Of course, I could help with documenting already existing functionalities. Latter won't happen immediately, probably gradually along with the actual integration and usage of gwt-jackson-apt.

One question here - do you look for documentation in README.md file or more extensive Javadoc or both?

@vegegoku
Copy link
Member

@tedynaidenov i would say i am looking for bot, in the readme we can go with a sample usage, and since we are into this ..i think it is the time for me to start working in a proper wiki pages for the lib.
i really appreciate the work you are doing here.

@ivmarkov
Copy link

Just a heads up that we'll be likely doing a few small changes to the code as well:

  • Type will be renamed to TypeToken<T> and will be generified
  • JsonRegistry will take the generified TypeToken<T> and will be quite a bit more type-safe as a result

@vegegoku
Copy link
Member

vegegoku commented Feb 4, 2019

@tedynaidenov Should we elaborate more on this PR, do you think we are ready to review and start merging?

@tedynaidenov
Copy link
Contributor Author

@vegegoku You can review the changes for this PR and eventually merge it. Currently we are working on an internal PoC including gwt-jackson-apt and autorest (https://github.com/intendia-oss/autorest). Once we are done, you can expect some examples and some updates for gwt-jackson-apt wiki.

@vegegoku
Copy link
Member

vegegoku commented Feb 5, 2019

Meanwhile i will find some time to prepare the wiki, thanks a lot

@tedynaidenov
Copy link
Contributor Author

@vegegoku I am about to create another PR to extend gwt-jackson-apt to deal with generic classes. It depends on changes introduced in this one. Could you please tell me how merging is going?

@vegegoku
Copy link
Member

Sorry, i didnt find enough time to go through it all ..and there was some conflict with changes introduced to fix an issue with the registry. i might find some time this weekend

@tedynaidenov
Copy link
Contributor Author

tedynaidenov commented Mar 13, 2019

@vegegoku Thanks a lot! In respect to the registry, probably you are talking about your recent "PECS" changes. I saw this conflict and I am not sure how TypeToken and PECS can live together in the registry.

Furthermore, in the next PR, we are going to have mappers for generic objects i.e.:

ObjectMapper<MyGenericClass<String, Integer>>

which will make things even more complicated. We have to elaborate on this...

@vegegoku
Copy link
Member

@tedynaidenov yes exactly .. i think that we need to push the registry out of the lib itself in its own module, and accordingly we might also think of different registry implementations but we need to split the concern, generating mapper is different from collecting them, what do you think?

@tedynaidenov
Copy link
Contributor Author

@vegegoku Well, I find the beauty of JSONRegistrationProcessor in its simplicity and small size, hence I have no concerns to see it living along with the ObjectMapperProcessor. But generally speaking you are right - object mapper generation is something very different from collecting them. Your statement holds true in the case we consider some serious development there.

I am struggling with another question. Can you please explain me the rational behind having PECS in the registry? Probably you were considering a use case, which I am missing.

@tedynaidenov
Copy link
Contributor Author

@vegegoku This weekend I spent some time thinking about the JsonRegistry and PECS and I think this might be somehow wrong.
Let's consider the "reader" case. If you declare a json reader for a class (using @JsonReader), you end up with reader implementation that creates specific deserializer(s) to read member fields of the given class (and members of it's super classes). This reader would not work if you try to use it to deserialize any sub or super objects of given class, right?

This reader is also referenced in the JsonRegistry. If you want to access the reader, you would request it from the JsonRegistry by Class key. I don't think relaxing of signature of getReader() and getWriter() methods won't do any good. For an example when you relax getReader() signature to

ObjectReader<? extends T> getReaders(Object<? extends T> key)

you allow clients to request a reader for subclass (as an example"GreenApple") and receive a reader for subclass (i.e. "Apple"). But that is wrong.

I think it would best if we setup a short session (via Hangouts, Skype, etc.) to elaborate on this and probably on TypeToken(s) in general. How do you think?

@vegegoku
Copy link
Member

This is a link to the discussion where the PECS was introduced
https://gitter.im/domino-gwt/Gwt-jackson-apt?at=5c4734bd20b78635b6651497

and TBH i am not a fan of the registry, yet i want this to implement this in the lib but as an separate module in the lib project.

i would confirm with the user f the registry if he still uses the PECS implementation and if he is not we can remove it, then we move the registry outside the main module.
most likely now he uses his own registry.

@vegegoku
Copy link
Member

@tedynaidenov the PECS changes have been reverted .. no we only need to go again through the PR for the last time before we merge it.

the next step after this is to move the registry to its own module.

@tedynaidenov
Copy link
Contributor Author

@vegegoku I have some good news - the patch for handling generic types is ready! I guess the best thing to submit it, is to open another PR.
If you have any other comments in respect to this PR, let me know so we can elaborate on them and eventually finalize the merge.

@ivmarkov
Copy link

@vegegoku - sorry to bother but... we have invested a lot of time and resources in this and it is a bit demotivating when we can't land a pull request for multiple months...

How can we speed up this process?

@vegegoku
Copy link
Member

@ivmarkov i am sorry for this, and really appreciate your work, i have a very limited time, but i will try to finish this today

@vegegoku vegegoku merged commit 7752548 into DominoKit:master Mar 27, 2019
@vegegoku
Copy link
Member

@tedynaidenov @ivmarkov
Thank you very much for the hard work and patience.
now the PR have been merged i really would like to have the registration to be moved to a new sub module before adding new features.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants