Skip to content

Allow generic base classes for POJOs #1423

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

Merged
merged 4 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions bson/src/main/org/bson/codecs/pojo/LazyPropertyModelCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -163,19 +163,44 @@ private <V> PropertyModel<V> getSpecializedPropertyModel(final PropertyModel<V>
static final class NeedSpecializationCodec<T> extends PojoCodec<T> {
private final ClassModel<T> classModel;
private final DiscriminatorLookup discriminatorLookup;
private final CodecRegistry codecRegistry;

NeedSpecializationCodec(final ClassModel<T> classModel, final DiscriminatorLookup discriminatorLookup) {
NeedSpecializationCodec(final ClassModel<T> classModel, final DiscriminatorLookup discriminatorLookup, final CodecRegistry codecRegistry) {
this.classModel = classModel;
this.discriminatorLookup = discriminatorLookup;
this.codecRegistry = codecRegistry;
}

@Override
public T decode(final BsonReader reader, final DecoderContext decoderContext) {
throw exception();
public void encode(final BsonWriter writer, final T value, final EncoderContext encoderContext) {
if (value.getClass().equals(classModel.getType())) {
throw exception();
}
tryEncode(codecRegistry.get(value.getClass()), writer, value, encoderContext);
}

@Override
public void encode(final BsonWriter writer, final T value, final EncoderContext encoderContext) {
public T decode(final BsonReader reader, final DecoderContext decoderContext) {
return tryDecode(reader, decoderContext);
}

@SuppressWarnings("unchecked")
private <A> void tryEncode(final Codec<A> codec, final BsonWriter writer, final T value, final EncoderContext encoderContext) {
try {
codec.encode(writer, (A) value, encoderContext);
} catch (Exception e) {
throw exception();
}
}

@SuppressWarnings("unchecked")
public T tryDecode(final BsonReader reader, final DecoderContext decoderContext) {
Codec<T> codec = PojoCodecImpl.<T>getCodecFromDocument(reader, classModel.useDiscriminator(), classModel.getDiscriminatorKey(),
codecRegistry, discriminatorLookup, null, classModel.getName());
if (codec != null) {
return codec.decode(reader, decoderContext);
}

throw exception();
}

Expand Down
18 changes: 10 additions & 8 deletions bson/src/main/org/bson/codecs/pojo/PojoCodecImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ public T decode(final BsonReader reader, final DecoderContext decoderContext) {
return instanceCreator.getInstance();
} else {
return getCodecFromDocument(reader, classModel.useDiscriminator(), classModel.getDiscriminatorKey(), registry,
discriminatorLookup, this).decode(reader, DecoderContext.builder().checkedDiscriminator(true).build());
discriminatorLookup, this, classModel.getName())
.decode(reader, DecoderContext.builder().checkedDiscriminator(true).build());
}
}

Expand Down Expand Up @@ -275,10 +276,11 @@ private <S, V> boolean areEquivalentTypes(final Class<S> t1, final Class<V> t2)
}

@SuppressWarnings("unchecked")
private Codec<T> getCodecFromDocument(final BsonReader reader, final boolean useDiscriminator, final String discriminatorKey,
final CodecRegistry registry, final DiscriminatorLookup discriminatorLookup,
final Codec<T> defaultCodec) {
Codec<T> codec = defaultCodec;
@Nullable
static <C> Codec<C> getCodecFromDocument(final BsonReader reader, final boolean useDiscriminator, final String discriminatorKey,
final CodecRegistry registry, final DiscriminatorLookup discriminatorLookup, @Nullable final Codec<C> defaultCodec,
final String simpleClassName) {
Codec<C> codec = defaultCodec;
if (useDiscriminator) {
BsonReaderMark mark = reader.getMark();
reader.readStartDocument();
Expand All @@ -289,12 +291,12 @@ private Codec<T> getCodecFromDocument(final BsonReader reader, final boolean use
discriminatorKeyFound = true;
try {
Class<?> discriminatorClass = discriminatorLookup.lookup(reader.readString());
if (!codec.getEncoderClass().equals(discriminatorClass)) {
codec = (Codec<T>) registry.get(discriminatorClass);
if (codec == null || !codec.getEncoderClass().equals(discriminatorClass)) {
codec = (Codec<C>) registry.get(discriminatorClass);
}
} catch (Exception e) {
throw new CodecConfigurationException(format("Failed to decode '%s'. Decoding errored with: %s",
classModel.getName(), e.getMessage()), e);
simpleClassName, e.getMessage()), e);
}
} else {
reader.skipValue();
Expand Down
2 changes: 1 addition & 1 deletion bson/src/main/org/bson/codecs/pojo/PojoCodecProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private static <T> PojoCodec<T> createCodec(final ClassModel<T> classModel, fina
final List<PropertyCodecProvider> propertyCodecProviders, final DiscriminatorLookup discriminatorLookup) {
return shouldSpecialize(classModel)
? new PojoCodecImpl<>(classModel, codecRegistry, propertyCodecProviders, discriminatorLookup)
: new LazyPropertyModelCodec.NeedSpecializationCodec<>(classModel, discriminatorLookup);
: new LazyPropertyModelCodec.NeedSpecializationCodec<>(classModel, discriminatorLookup, codecRegistry);
}

/**
Expand Down
16 changes: 15 additions & 1 deletion bson/src/test/unit/org/bson/codecs/pojo/PojoCustomTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@
import org.bson.codecs.pojo.entities.BsonRepresentationUnsupportedString;
import org.bson.codecs.pojo.entities.ConcreteAndNestedAbstractInterfaceModel;
import org.bson.codecs.pojo.entities.ConcreteCollectionsModel;
import org.bson.codecs.pojo.entities.ConcreteModel;
import org.bson.codecs.pojo.entities.ConcreteField;
import org.bson.codecs.pojo.entities.ConcreteStandAloneAbstractInterfaceModel;
import org.bson.codecs.pojo.entities.ConstructorNotPublicModel;
import org.bson.codecs.pojo.entities.ConventionModel;
import org.bson.codecs.pojo.entities.ConverterModel;
import org.bson.codecs.pojo.entities.CustomPropertyCodecOptionalModel;
import org.bson.codecs.pojo.entities.GenericBaseModel;
import org.bson.codecs.pojo.entities.GenericHolderModel;
import org.bson.codecs.pojo.entities.GenericTreeModel;
import org.bson.codecs.pojo.entities.InterfaceBasedModel;
Expand Down Expand Up @@ -545,6 +548,17 @@ public void testInvalidDiscriminatorInNestedModel() {
+ "'simple': {'_t': 'FakeModel', 'integerField': 42, 'stringField': 'myString'}}"));
}

@Test
public void testGenericBaseClass() {
CodecRegistry registry = fromProviders(new ValueCodecProvider(), PojoCodecProvider.builder().automatic(true).build());

ConcreteModel model = new ConcreteModel(new ConcreteField("name1"));

String json = "{\"_t\": \"org.bson.codecs.pojo.entities.ConcreteModel\", \"field\": {\"name\": \"name1\"}}";
roundTrip(PojoCodecProvider.builder().automatic(true), GenericBaseModel.class, model, json);
}


@Test
public void testCannotEncodeUnspecializedClasses() {
CodecRegistry registry = fromProviders(getPojoCodecProviderBuilder(GenericTreeModel.class).build());
Expand All @@ -553,7 +567,7 @@ public void testCannotEncodeUnspecializedClasses() {
}

@Test
public void testCannotDecodeUnspecializedClasses() {
public void testCannotDecodeUnspecializedClassesWithoutADiscriminator() {
assertThrows(CodecConfigurationException.class, () ->
decodingShouldFail(getCodec(GenericTreeModel.class),
"{'field1': 'top', 'field2': 1, "
Expand Down
32 changes: 25 additions & 7 deletions bson/src/test/unit/org/bson/codecs/pojo/PojoTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,12 @@ <T> void roundTrip(final T value, final String json) {
}

<T> void roundTrip(final PojoCodecProvider.Builder builder, final T value, final String json) {
encodesTo(getCodecRegistry(builder), value, json);
decodesTo(getCodecRegistry(builder), json, value);
roundTrip(builder, value.getClass(), value, json);
}

<T> void roundTrip(final PojoCodecProvider.Builder builder, final Class<?> clazz, final T value, final String json) {
encodesTo(getCodecRegistry(builder), clazz, value, json);
decodesTo(getCodecRegistry(builder), clazz, json, value);
}

<T> void threadedRoundTrip(final PojoCodecProvider.Builder builder, final T value, final String json) {
Expand All @@ -109,21 +113,30 @@ <T> void roundTrip(final CodecRegistry registry, final T value, final String jso
decodesTo(registry, json, value);
}

<T> void roundTrip(final CodecRegistry registry, final Class<T> clazz, final T value, final String json) {
encodesTo(registry, clazz, value, json);
decodesTo(registry, clazz, json, value);
}

<T> void encodesTo(final PojoCodecProvider.Builder builder, final T value, final String json) {
encodesTo(builder, value, json, false);
}

<T> void encodesTo(final PojoCodecProvider.Builder builder, final T value, final String json, final boolean collectible) {
encodesTo(getCodecRegistry(builder), value, json, collectible);
encodesTo(getCodecRegistry(builder), value.getClass(), value, json, collectible);
}

<T> void encodesTo(final CodecRegistry registry, final T value, final String json) {
encodesTo(registry, value, json, false);
encodesTo(registry, value.getClass(), value, json, false);
}

<T> void encodesTo(final CodecRegistry registry, final Class<?> clazz, final T value, final String json) {
encodesTo(registry, clazz, value, json, false);
}

@SuppressWarnings("unchecked")
<T> void encodesTo(final CodecRegistry registry, final T value, final String json, final boolean collectible) {
Codec<T> codec = (Codec<T>) registry.get(value.getClass());
<T> void encodesTo(final CodecRegistry registry, final Class<?> clazz, final T value, final String json, final boolean collectible) {
Codec<T> codec = (Codec<T>) registry.get(clazz);
encodesTo(codec, value, json, collectible);
}

Expand All @@ -144,7 +157,12 @@ <T> void decodesTo(final PojoCodecProvider.Builder builder, final String json, f

@SuppressWarnings("unchecked")
<T> void decodesTo(final CodecRegistry registry, final String json, final T expected) {
Codec<T> codec = (Codec<T>) registry.get(expected.getClass());
decodesTo(registry, expected.getClass(), json, expected);
}

@SuppressWarnings("unchecked")
<T> void decodesTo(final CodecRegistry registry, final Class<?> clazz, final String json, final T expected) {
Codec<T> codec = (Codec<T>) registry.get(clazz);
decodesTo(codec, json, expected);
}

Expand Down
55 changes: 55 additions & 0 deletions bson/src/test/unit/org/bson/codecs/pojo/entities/BaseField.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.codecs.pojo.entities;

import java.util.Objects;

public abstract class BaseField {
private String name;

public BaseField(final String name) {
this.name = name;
}

protected BaseField() {
}

public String getName() {
return name;
}

public void setName(final String name) {
this.name = name;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BaseField baseField = (BaseField) o;
return Objects.equals(name, baseField.name);
}

@Override
public int hashCode() {
return Objects.hashCode(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.codecs.pojo.entities;

public class ConcreteField extends BaseField {

public ConcreteField() {
}

public ConcreteField(final String name) {
super(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.codecs.pojo.entities;

public class ConcreteModel extends GenericBaseModel<ConcreteField> {

public ConcreteModel() {
}

public ConcreteModel(final ConcreteField field) {
super(field);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.bson.codecs.pojo.entities;

import org.bson.codecs.pojo.annotations.BsonDiscriminator;

import java.util.Objects;

@BsonDiscriminator()
public class GenericBaseModel<T extends BaseField> {

private T field;

public GenericBaseModel(final T field) {
this.field = field;
}

public GenericBaseModel() {
}

public T getField() {
return field;
}

public void setField(final T field) {
this.field = field;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
GenericBaseModel<?> that = (GenericBaseModel<?>) o;
return Objects.equals(field, that.field);
}

@Override
public int hashCode() {
return Objects.hashCode(field);
}
}