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

Support List<T> and Map<String, T> components in Java records #1002

Merged
merged 4 commits into from
Sep 27, 2022

Conversation

jyemin
Copy link
Collaborator

@jyemin jyemin commented Sep 23, 2022

This enhances the support for any record component that is a generic List or Map where the generic type of the List or Map value is a record or POJO. It used to incorrectly decode to org.bson.Document. Now it respects the generic type of the List or Map value when decoding.

JAVA-4667

@jyemin jyemin requested a review from rozza September 23, 2022 16:01
@jyemin
Copy link
Collaborator Author

jyemin commented Sep 23, 2022

Particularly looking for feedback on a new Parameterizable interface for Codecs as an improved solution to the problem of parameterizing a Codec with generic types.

If it looks good I'll proceed with adding more tests.

@jyemin jyemin requested a review from katcharov September 23, 2022 16:03
@jyemin jyemin changed the title Support List<T> components in Java records Support List<T> and Map<String, T> components in Java records Sep 23, 2022
This enhances the support for any record component that is a
generic List or Map where the generic type of the List or Map
is a record or POJO.  It used to incorrectly decode to
org.bson.Document. Now it respects the generic type of the
List or Map when decoding.

JAVA-4667
@jyemin jyemin marked this pull request as ready for review September 25, 2022 12:39
import java.util.ArrayList;
import java.util.List;

abstract class AbstractIterableCodec<T> implements Codec<Iterable<T>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Extracted superclass so that the main logic can be shared between IterableCodec and the new ParameterizedIterableCodec.

import java.util.HashMap;
import java.util.Map;

public abstract class AbstractMapCodec<T> implements Codec<Map<String, T>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same here.

@@ -62,6 +66,23 @@ static Object readValue(final BsonReader reader, final DecoderContext decoderCon
}
}

static Codec<?> getCodec(final CodecRegistry codecRegistry, final Type type) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the guts of the implementation: a recursive algorithm for creating a parameterized codec of any depth.

Copy link
Member

Choose a reason for hiding this comment

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

Seems familiar to the Pojo codec parameterizedType handling but cleaner.


/**
* Encodes and decodes {@code Iterable} objects.
*
* @since 3.3
*/
@SuppressWarnings("rawtypes")
public class IterableCodec implements Codec<Iterable>, OverridableUuidRepresentationCodec<Iterable> {
public class IterableCodec extends AbstractIterableCodec<Object>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

IterableCodec now implements Codec<Iterable<Object>> instead of Codec<Iterable>. This is binary but not source compatibile, which I think is ok given how this class is likely (not) used directly in applications.

@@ -79,48 +79,31 @@ public Object transform(final Object objectToTransform) {


@Override
public Codec<Iterable> withUuidRepresentation(final UuidRepresentation uuidRepresentation) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The diffs are a bit ugly due to the mix of functional changes with refactoring. I didn't realize I would need to extract a superclass until after the functional work was done. If it's too hard to read, I can go back and rebase everything, doing the refactoring first in a separate PR.

return codecRegistry.get((Class<?>) type);
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Codec<?> rawCodec = codecRegistry.get((Class<?>) parameterizedType.getRawType());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ParameterizedType#getRawType returns Type, not Class, but can the raw type of a parameterized type be anything else except a class? If so, this is an unsafe cast.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I see that sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl's rawType field is of type Class<?>, which is a clue that this is safe.

Copy link
Member

@rozza rozza left a comment

Choose a reason for hiding this comment

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

Can you add a ParameterizedRecordCodecas well then top level records can contain nested generic records eg:

public record TestRecordWithListGenericConcrete(@BsonId ObjectId id, TestRecordWithListGeneric<TestRecordEmbedded> nestedRecords) {
}

public record TestRecordWithListGeneric<T>(List<T> nestedRecords) {
}

@@ -83,6 +85,10 @@ Object getValue(final Record record) throws InvocationTargetException, IllegalAc
@SuppressWarnings("deprecation")
private static Codec<?> computeCodec(final RecordComponent component, final CodecRegistry codecRegistry) {
var codec = codecRegistry.get(toWrapper(component.getType()));
if (codec instanceof Parameterizable parameterizableCodec
Copy link
Member

Choose a reason for hiding this comment

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

What is this magic? assigning variables in an if statement 🤯

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not just one but two!

@@ -62,6 +66,23 @@ static Object readValue(final BsonReader reader, final DecoderContext decoderCon
}
}

static Codec<?> getCodec(final CodecRegistry codecRegistry, final Type type) {
Copy link
Member

Choose a reason for hiding this comment

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

Seems familiar to the Pojo codec parameterizedType handling but cleaner.

@jyemin
Copy link
Collaborator Author

jyemin commented Sep 27, 2022

Can you add a ParameterizedRecordCodecas well then top level records can contain nested generic records

Yes, I don't see why not. The implementation will be a bit more complicated but I think it fits in with the overall design. I'll open a follow-up ticket for that if you don't mind.

@jyemin jyemin requested a review from rozza September 27, 2022 13:19
Copy link
Member

@rozza rozza left a comment

Choose a reason for hiding this comment

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

LGTM!

@jyemin
Copy link
Collaborator Author

jyemin commented Sep 27, 2022

Opened JAVA-4740 to track request for support of nested parameterized records.

Copy link
Contributor

@katcharov katcharov left a comment

Choose a reason for hiding this comment

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

lgtm!

@jyemin jyemin merged commit d1fec61 into mongodb:master Sep 27, 2022
@jyemin jyemin deleted the j4667 branch September 27, 2022 16:45
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.

3 participants