diff --git a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java index ce865d23f1d..3a619639cf6 100644 --- a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java +++ b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java @@ -22,6 +22,7 @@ import org.bson.codecs.Codec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; +import org.bson.codecs.Parameterizable; import org.bson.codecs.RepresentationConfigurable; import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; @@ -35,6 +36,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.RecordComponent; import java.util.ArrayList; import java.util.Arrays; @@ -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 + && component.getGenericType() instanceof ParameterizedType parameterizedType) { + codec = parameterizableCodec.parameterize(codecRegistry, Arrays.asList(parameterizedType.getActualTypeArguments())); + } BsonType bsonRepresentationType = null; if (component.isAnnotationPresent(BsonRepresentation.class)) { diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java index 636496c6cb6..fceb68e5b2d 100644 --- a/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java @@ -26,6 +26,7 @@ import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.codecs.configuration.CodecConfigurationException; +import org.bson.codecs.record.samples.TestRecordEmbedded; import org.bson.codecs.record.samples.TestRecordWithDeprecatedAnnotations; import org.bson.codecs.record.samples.TestRecordWithIllegalBsonCreatorOnConstructor; import org.bson.codecs.record.samples.TestRecordWithIllegalBsonCreatorOnMethod; @@ -39,13 +40,19 @@ import org.bson.codecs.record.samples.TestRecordWithIllegalBsonPropertyOnAccessor; import org.bson.codecs.record.samples.TestRecordWithIllegalBsonPropertyOnCanonicalConstructor; import org.bson.codecs.record.samples.TestRecordWithIllegalBsonRepresentationOnAccessor; +import org.bson.codecs.record.samples.TestRecordWithListOfListOfRecords; +import org.bson.codecs.record.samples.TestRecordWithListOfRecords; +import org.bson.codecs.record.samples.TestRecordWithMapOfListOfRecords; +import org.bson.codecs.record.samples.TestRecordWithMapOfRecords; import org.bson.codecs.record.samples.TestRecordWithPojoAnnotations; import org.bson.conversions.Bson; import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import java.util.List; +import java.util.Map; +import static org.bson.codecs.configuration.CodecRegistries.fromProviders; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -107,6 +114,119 @@ public void testRecordWithPojoAnnotations() { assertEquals(testRecord, decoded); } + @Test + public void testRecordWithNestedListOfRecords() { + var codec = new RecordCodec<>(TestRecordWithListOfRecords.class, + fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); + var identifier = new ObjectId(); + var testRecord = new TestRecordWithListOfRecords(identifier, List.of(new TestRecordEmbedded("embedded"))); + + var document = new BsonDocument(); + var writer = new BsonDocumentWriter(document); + + // when + codec.encode(writer, testRecord, EncoderContext.builder().build()); + + // then + assertEquals( + new BsonDocument("_id", new BsonObjectId(identifier)) + .append("nestedRecords", new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded"))))), + document); + assertEquals("_id", document.getFirstKey()); + + // when + var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build()); + + // then + assertEquals(testRecord, decoded); + } + + @Test + public void testRecordWithNestedListOfListOfRecords() { + var codec = new RecordCodec<>(TestRecordWithListOfListOfRecords.class, + fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); + var identifier = new ObjectId(); + var testRecord = new TestRecordWithListOfListOfRecords(identifier, List.of(List.of(new TestRecordEmbedded("embedded")))); + + var document = new BsonDocument(); + var writer = new BsonDocumentWriter(document); + + // when + codec.encode(writer, testRecord, EncoderContext.builder().build()); + + // then + assertEquals( + new BsonDocument("_id", new BsonObjectId(identifier)) + .append("nestedRecords", + new BsonArray(List.of(new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded"))))))), + document); + assertEquals("_id", document.getFirstKey()); + + // when + var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build()); + + // then + assertEquals(testRecord, decoded); + } + + @Test + public void testRecordWithNestedMapOfRecords() { + var codec = new RecordCodec<>(TestRecordWithMapOfRecords.class, + fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); + var identifier = new ObjectId(); + var testRecord = new TestRecordWithMapOfRecords(identifier, + Map.of("first", new TestRecordEmbedded("embedded"))); + + var document = new BsonDocument(); + var writer = new BsonDocumentWriter(document); + + // when + codec.encode(writer, testRecord, EncoderContext.builder().build()); + + // then + assertEquals( + new BsonDocument("_id", new BsonObjectId(identifier)) + .append("nestedRecords", new BsonDocument("first", new BsonDocument("name", new BsonString("embedded")))), + document); + assertEquals("_id", document.getFirstKey()); + + // when + var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build()); + + // then + assertEquals(testRecord, decoded); + } + + @Test + public void testRecordWithNestedMapOfListRecords() { + var codec = new RecordCodec<>(TestRecordWithMapOfListOfRecords.class, + fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); + var identifier = new ObjectId(); + var testRecord = new TestRecordWithMapOfListOfRecords(identifier, + Map.of("first", List.of(new TestRecordEmbedded("embedded")))); + + var document = new BsonDocument(); + var writer = new BsonDocumentWriter(document); + + // when + codec.encode(writer, testRecord, EncoderContext.builder().build()); + + // then + assertEquals( + new BsonDocument("_id", new BsonObjectId(identifier)) + .append("nestedRecords", + new BsonDocument("first", + new BsonArray(List.of(new BsonDocument("name", new BsonString("embedded")))))), + document); + assertEquals("_id", document.getFirstKey()); + + // when + var decoded = codec.decode(new BsonDocumentReader(document), DecoderContext.builder().build()); + + // then + assertEquals(testRecord, decoded); + } + @Test public void testRecordWithNulls() { var codec = new RecordCodec<>(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordEmbedded.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordEmbedded.java new file mode 100644 index 00000000000..b83f6bde2e2 --- /dev/null +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordEmbedded.java @@ -0,0 +1,20 @@ +/* + * 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.record.samples; + +public record TestRecordEmbedded(String name) { +} diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithListOfListOfRecords.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithListOfListOfRecords.java new file mode 100644 index 00000000000..65012c32fbb --- /dev/null +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithListOfListOfRecords.java @@ -0,0 +1,25 @@ +/* + * 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.record.samples; + +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +import java.util.List; + +public record TestRecordWithListOfListOfRecords(@BsonId ObjectId id, List> nestedRecords) { +} diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithListOfRecords.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithListOfRecords.java new file mode 100644 index 00000000000..459186e863c --- /dev/null +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithListOfRecords.java @@ -0,0 +1,25 @@ +/* + * 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.record.samples; + +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +import java.util.List; + +public record TestRecordWithListOfRecords(@BsonId ObjectId id, List nestedRecords) { +} diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithMapOfListOfRecords.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithMapOfListOfRecords.java new file mode 100644 index 00000000000..b9b220b9579 --- /dev/null +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithMapOfListOfRecords.java @@ -0,0 +1,26 @@ +/* + * 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.record.samples; + +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +import java.util.List; +import java.util.Map; + +public record TestRecordWithMapOfListOfRecords(@BsonId ObjectId id, Map> nestedRecords) { +} diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithMapOfRecords.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithMapOfRecords.java new file mode 100644 index 00000000000..5989fdbb085 --- /dev/null +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/samples/TestRecordWithMapOfRecords.java @@ -0,0 +1,25 @@ +/* + * 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.record.samples; + +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +import java.util.Map; + +public record TestRecordWithMapOfRecords(@BsonId ObjectId id, Map nestedRecords) { +} diff --git a/bson/src/main/org/bson/codecs/AbstractIterableCodec.java b/bson/src/main/org/bson/codecs/AbstractIterableCodec.java new file mode 100644 index 00000000000..4dd78b5bd51 --- /dev/null +++ b/bson/src/main/org/bson/codecs/AbstractIterableCodec.java @@ -0,0 +1,69 @@ +/* + * 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; + +import org.bson.BsonReader; +import org.bson.BsonType; +import org.bson.BsonWriter; + +import java.util.ArrayList; +import java.util.List; + +abstract class AbstractIterableCodec implements Codec> { + + abstract T readValue(BsonReader reader, DecoderContext decoderContext); + + abstract void writeValue(BsonWriter writer, T cur, EncoderContext encoderContext); + + @Override + public Iterable decode(final BsonReader reader, final DecoderContext decoderContext) { + reader.readStartArray(); + + List list = new ArrayList<>(); + while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { + if (reader.getCurrentBsonType() == BsonType.NULL) { + reader.readNull(); + list.add(null); + } else { + list.add(readValue(reader, decoderContext)); + } + } + + reader.readEndArray(); + + return list; + } + + @Override + public void encode(final BsonWriter writer, final Iterable value, final EncoderContext encoderContext) { + writer.writeStartArray(); + for (final T cur : value) { + if (cur == null) { + writer.writeNull(); + } else { + writeValue(writer, cur, encoderContext); + } + } + writer.writeEndArray(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Class> getEncoderClass() { + return (Class>) ((Class) Iterable.class); + } +} diff --git a/bson/src/main/org/bson/codecs/AbstractMapCodec.java b/bson/src/main/org/bson/codecs/AbstractMapCodec.java new file mode 100644 index 00000000000..60b48cabefc --- /dev/null +++ b/bson/src/main/org/bson/codecs/AbstractMapCodec.java @@ -0,0 +1,72 @@ +/* + * 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; + +import org.bson.BsonReader; +import org.bson.BsonType; +import org.bson.BsonWriter; + +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractMapCodec implements Codec> { + + abstract T readValue(BsonReader reader, DecoderContext decoderContext); + + abstract void writeValue(BsonWriter writer, T value, EncoderContext encoderContext); + + @Override + public void encode(final BsonWriter writer, final Map map, final EncoderContext encoderContext) { + writer.writeStartDocument(); + for (final Map.Entry entry : map.entrySet()) { + writer.writeName(entry.getKey()); + T value = entry.getValue(); + if (value == null) { + writer.writeNull(); + } else { + writeValue(writer, value, encoderContext); + } + } + writer.writeEndDocument(); + } + + + @Override + public Map decode(final BsonReader reader, final DecoderContext decoderContext) { + Map map = new HashMap<>(); + + reader.readStartDocument(); + while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { + String fieldName = reader.readName(); + if (reader.getCurrentBsonType() == BsonType.NULL) { + reader.readNull(); + map.put(fieldName, null); + } else { + map.put(fieldName, readValue(reader, decoderContext)); + } + } + + reader.readEndDocument(); + return map; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public Class> getEncoderClass() { + return (Class>) ((Class) Map.class); + } +} diff --git a/bson/src/main/org/bson/codecs/ContainerCodecHelper.java b/bson/src/main/org/bson/codecs/ContainerCodecHelper.java index d0303e563fd..6f9573ffe02 100644 --- a/bson/src/main/org/bson/codecs/ContainerCodecHelper.java +++ b/bson/src/main/org/bson/codecs/ContainerCodecHelper.java @@ -20,8 +20,12 @@ import org.bson.BsonType; import org.bson.Transformer; import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; import java.util.UUID; /** @@ -62,6 +66,23 @@ static Object readValue(final BsonReader reader, final DecoderContext decoderCon } } + static Codec getCodec(final CodecRegistry codecRegistry, final Type type) { + if (type instanceof Class) { + return codecRegistry.get((Class) type); + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Codec rawCodec = codecRegistry.get((Class) parameterizedType.getRawType()); + if (rawCodec instanceof Parameterizable) { + return ((Parameterizable) rawCodec).parameterize(codecRegistry, Arrays.asList(parameterizedType.getActualTypeArguments())); + } else { + return rawCodec; + } + } else { + throw new CodecConfigurationException("Unsupported generic type of container: " + type); + } + } + + private ContainerCodecHelper() { } } diff --git a/bson/src/main/org/bson/codecs/IterableCodec.java b/bson/src/main/org/bson/codecs/IterableCodec.java index 136941a13c6..c231dc67e4c 100644 --- a/bson/src/main/org/bson/codecs/IterableCodec.java +++ b/bson/src/main/org/bson/codecs/IterableCodec.java @@ -17,17 +17,17 @@ package org.bson.codecs; import org.bson.BsonReader; -import org.bson.BsonType; import org.bson.BsonWriter; import org.bson.Transformer; import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; -import java.util.ArrayList; +import java.lang.reflect.Type; import java.util.List; import static org.bson.assertions.Assertions.notNull; -import static org.bson.codecs.ContainerCodecHelper.readValue; +import static org.bson.codecs.ContainerCodecHelper.getCodec; /** * Encodes and decodes {@code Iterable} objects. @@ -35,7 +35,8 @@ * @since 3.3 */ @SuppressWarnings("rawtypes") -public class IterableCodec implements Codec, OverridableUuidRepresentationCodec { +public class IterableCodec extends AbstractIterableCodec + implements OverridableUuidRepresentationCodec>, Parameterizable { private final CodecRegistry registry; private final BsonTypeCodecMap bsonTypeCodecMap; @@ -63,7 +64,6 @@ public IterableCodec(final CodecRegistry registry, final BsonTypeClassMap bsonTy this(registry, new BsonTypeCodecMap(notNull("bsonTypeClassMap", bsonTypeClassMap), registry), valueTransformer, UuidRepresentation.UNSPECIFIED); } - private IterableCodec(final CodecRegistry registry, final BsonTypeCodecMap bsonTypeCodecMap, final Transformer valueTransformer, final UuidRepresentation uuidRepresentation) { this.registry = notNull("registry", registry); @@ -79,48 +79,31 @@ public Object transform(final Object objectToTransform) { @Override - public Codec withUuidRepresentation(final UuidRepresentation uuidRepresentation) { - if (this.uuidRepresentation.equals(uuidRepresentation)) { - return this; + public Codec parameterize(final CodecRegistry codecRegistry, final List types) { + if (types.size() != 1) { + throw new CodecConfigurationException("Expected only one parameterized type for an Iterable, but found " + types.size()); } - return new IterableCodec(registry, bsonTypeCodecMap, valueTransformer, uuidRepresentation); - } - - @Override - public Iterable decode(final BsonReader reader, final DecoderContext decoderContext) { - reader.readStartArray(); - List list = new ArrayList(); - while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { - list.add(readValue(reader, decoderContext, bsonTypeCodecMap, uuidRepresentation, registry, valueTransformer)); - } - - reader.readEndArray(); - - return list; + return new ParameterizedIterableCodec<>(getCodec(codecRegistry, types.get(0))); } @Override - public void encode(final BsonWriter writer, final Iterable value, final EncoderContext encoderContext) { - writer.writeStartArray(); - for (final Object cur : value) { - writeValue(writer, encoderContext, cur); + public Codec> withUuidRepresentation(final UuidRepresentation uuidRepresentation) { + if (this.uuidRepresentation.equals(uuidRepresentation)) { + return this; } - writer.writeEndArray(); + return new IterableCodec(registry, bsonTypeCodecMap, valueTransformer, uuidRepresentation); } @Override - public Class getEncoderClass() { - return Iterable.class; + Object readValue(final BsonReader reader, final DecoderContext decoderContext) { + return ContainerCodecHelper.readValue(reader, decoderContext, bsonTypeCodecMap, uuidRepresentation, registry, valueTransformer); } - @SuppressWarnings({"unchecked", "rawtypes"}) - private void writeValue(final BsonWriter writer, final EncoderContext encoderContext, final Object value) { - if (value == null) { - writer.writeNull(); - } else { - Codec codec = registry.get(value.getClass()); - encoderContext.encodeWithChildContext(codec, writer, value); - } + @SuppressWarnings("unchecked") + @Override + void writeValue(final BsonWriter writer, final Object value, final EncoderContext encoderContext) { + Codec codec = registry.get(value.getClass()); + encoderContext.encodeWithChildContext(codec, writer, value); } } diff --git a/bson/src/main/org/bson/codecs/MapCodec.java b/bson/src/main/org/bson/codecs/MapCodec.java index 6a0e5c9263b..54008bdbe23 100644 --- a/bson/src/main/org/bson/codecs/MapCodec.java +++ b/bson/src/main/org/bson/codecs/MapCodec.java @@ -17,18 +17,19 @@ package org.bson.codecs; import org.bson.BsonReader; -import org.bson.BsonType; import org.bson.BsonWriter; import org.bson.Transformer; import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; -import java.util.HashMap; +import java.lang.reflect.Type; +import java.util.List; import java.util.Map; import static java.util.Arrays.asList; import static org.bson.assertions.Assertions.notNull; -import static org.bson.codecs.ContainerCodecHelper.readValue; +import static org.bson.codecs.ContainerCodecHelper.getCodec; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; /** @@ -36,7 +37,7 @@ * * @since 3.5 */ -public class MapCodec implements Codec>, OverridableUuidRepresentationCodec> { +public class MapCodec extends AbstractMapCodec implements OverridableUuidRepresentationCodec>, Parameterizable { private static final CodecRegistry DEFAULT_REGISTRY = fromProviders(asList(new ValueCodecProvider(), new BsonValueCodecProvider(), new DocumentCodecProvider(), new IterableCodecProvider(), new MapCodecProvider())); @@ -108,42 +109,27 @@ public Codec> withUuidRepresentation(final UuidRepresentatio } @Override - public void encode(final BsonWriter writer, final Map map, final EncoderContext encoderContext) { - writer.writeStartDocument(); - for (final Map.Entry entry : map.entrySet()) { - writer.writeName(entry.getKey()); - writeValue(writer, encoderContext, entry.getValue()); + public Codec parameterize(final CodecRegistry codecRegistry, final List types) { + if (types.size() != 2) { + throw new CodecConfigurationException("Expected two parameterized type for an Iterable, but found " + + types.size()); } - writer.writeEndDocument(); - } - - @Override - public Map decode(final BsonReader reader, final DecoderContext decoderContext) { - Map map = new HashMap(); - - reader.readStartDocument(); - while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { - String fieldName = reader.readName(); - map.put(fieldName, readValue(reader, decoderContext, bsonTypeCodecMap, uuidRepresentation, registry, valueTransformer)); + Type genericTypeOfMapKey = types.get(0); + if (!genericTypeOfMapKey.getTypeName().equals("java.lang.String")) { + throw new CodecConfigurationException("Unsupported key type for Map: " + genericTypeOfMapKey.getTypeName()); } - - reader.readEndDocument(); - return map; + return new ParameterizedMapCodec<>(getCodec(codecRegistry, types.get(1))); } - @SuppressWarnings("unchecked") @Override - public Class> getEncoderClass() { - return (Class>) ((Class) Map.class); + Object readValue(final BsonReader reader, final DecoderContext decoderContext) { + return ContainerCodecHelper.readValue(reader, decoderContext, bsonTypeCodecMap, uuidRepresentation, registry, valueTransformer); } - @SuppressWarnings({"unchecked", "rawtypes"}) - private void writeValue(final BsonWriter writer, final EncoderContext encoderContext, final Object value) { - if (value == null) { - writer.writeNull(); - } else { - Codec codec = registry.get(value.getClass()); - encoderContext.encodeWithChildContext(codec, writer, value); - } + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + void writeValue(final BsonWriter writer, final Object value, final EncoderContext encoderContext) { + Codec codec = registry.get(value.getClass()); + encoderContext.encodeWithChildContext(codec, writer, value); } } diff --git a/bson/src/main/org/bson/codecs/Parameterizable.java b/bson/src/main/org/bson/codecs/Parameterizable.java new file mode 100644 index 00000000000..0b6f219d9da --- /dev/null +++ b/bson/src/main/org/bson/codecs/Parameterizable.java @@ -0,0 +1,38 @@ +/* + * 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; + +import org.bson.codecs.configuration.CodecRegistry; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * An interface indicating that a Codec is for a type that can be parameterized by generic types. + * + * @since 4.8 + */ +public interface Parameterizable { + /** + * Recursively parameterize the codec with the given registry and generic type arguments. + * + * @param codecRegistry the code registry to use to resolve codecs for the generic type arguments + * @param types the types that are parameterizing the containing type. + * @return the Codec parameterized with the given types + */ + Codec parameterize(CodecRegistry codecRegistry, List types); +} diff --git a/bson/src/main/org/bson/codecs/ParameterizedIterableCodec.java b/bson/src/main/org/bson/codecs/ParameterizedIterableCodec.java new file mode 100644 index 00000000000..47ee562480c --- /dev/null +++ b/bson/src/main/org/bson/codecs/ParameterizedIterableCodec.java @@ -0,0 +1,38 @@ +/* + * 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; + +import org.bson.BsonReader; +import org.bson.BsonWriter; + +class ParameterizedIterableCodec extends AbstractIterableCodec { + private final Codec codec; + + ParameterizedIterableCodec(final Codec codec) { + this.codec = codec; + } + + @Override + T readValue(final BsonReader reader, final DecoderContext decoderContext) { + return decoderContext.decodeWithChildContext(codec, reader); + } + + @Override + void writeValue(final BsonWriter writer, final T cur, final EncoderContext encoderContext) { + encoderContext.encodeWithChildContext(codec, writer, cur); + } +} diff --git a/bson/src/main/org/bson/codecs/ParameterizedMapCodec.java b/bson/src/main/org/bson/codecs/ParameterizedMapCodec.java new file mode 100644 index 00000000000..d27fb604c4d --- /dev/null +++ b/bson/src/main/org/bson/codecs/ParameterizedMapCodec.java @@ -0,0 +1,43 @@ +/* + * 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; + +import org.bson.BsonReader; +import org.bson.BsonWriter; + +/** + * A Codec for Map instances. + * + * @since 3.5 + */ +class ParameterizedMapCodec extends AbstractMapCodec { + private final Codec codec; + + ParameterizedMapCodec(final Codec codec) { + this.codec = codec; + } + + @Override + T readValue(final BsonReader reader, final DecoderContext decoderContext) { + return decoderContext.decodeWithChildContext(codec, reader); + } + + @Override + void writeValue(final BsonWriter writer, final T value, final EncoderContext encoderContext) { + encoderContext.encodeWithChildContext(codec, writer, value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/IterableCodecSpecification.groovy b/bson/src/test/unit/org/bson/codecs/IterableCodecSpecification.groovy index 1a83a271f12..3214f5d5d24 100644 --- a/bson/src/test/unit/org/bson/codecs/IterableCodecSpecification.groovy +++ b/bson/src/test/unit/org/bson/codecs/IterableCodecSpecification.groovy @@ -16,13 +16,19 @@ package org.bson.codecs +import org.bson.BsonArray +import org.bson.BsonDateTime import org.bson.BsonDocument import org.bson.BsonDocumentReader import org.bson.BsonDocumentWriter +import org.bson.codecs.jsr310.Jsr310CodecProvider import org.bson.types.Binary import spock.lang.Specification import spock.lang.Unroll +import java.lang.reflect.ParameterizedType +import java.time.Instant + import static org.bson.BsonDocument.parse import static org.bson.UuidRepresentation.C_SHARP_LEGACY import static org.bson.UuidRepresentation.JAVA_LEGACY @@ -36,7 +42,8 @@ import static org.bson.codecs.configuration.CodecRegistries.fromRegistries class IterableCodecSpecification extends Specification { static final REGISTRY = fromRegistries(fromCodecs(new UuidCodec(JAVA_LEGACY)), - fromProviders(new ValueCodecProvider(), new DocumentCodecProvider(), new BsonValueCodecProvider(), new IterableCodecProvider())) + fromProviders(new ValueCodecProvider(), new DocumentCodecProvider(), new BsonValueCodecProvider(), + new IterableCodecProvider(), new MapCodecProvider())) def 'should have Iterable encoding class'() { given: @@ -159,4 +166,46 @@ class IterableCodecSpecification extends Specification { PYTHON_LEGACY | [new Binary((byte) 4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] as byte[])] | '{"array": [{ "$binary" : "AQIDBAUGBwgJCgsMDQ4PEA==", "$type" : "4" }]}' UNSPECIFIED | [new Binary((byte) 4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] as byte[])] | '{"array": [{ "$binary" : "AQIDBAUGBwgJCgsMDQ4PEA==", "$type" : "4" }]}' } + + def 'should parameterize'() { + given: + def codec = new IterableCodec(REGISTRY, new BsonTypeClassMap()) + def writer = new BsonDocumentWriter(new BsonDocument()) + def reader = new BsonDocumentReader(writer.getDocument()) + def instants = [ + ['firstMap': [Instant.ofEpochMilli(1), Instant.ofEpochMilli(2)]], + ['secondMap': [Instant.ofEpochMilli(3), Instant.ofEpochMilli(4)]]] + when: + codec = codec.parameterize(fromProviders(new Jsr310CodecProvider(), REGISTRY), + Arrays.asList(((ParameterizedType) Container.getMethod('getInstants').genericReturnType).actualTypeArguments)) + writer.writeStartDocument() + writer.writeName('instants') + codec.encode(writer, instants, EncoderContext.builder().build()) + writer.writeEndDocument() + + then: + writer.getDocument() == new BsonDocument() + .append('instants', new BsonArray( + [ + new BsonDocument('firstMap', new BsonArray([new BsonDateTime(1), new BsonDateTime(2)])), + new BsonDocument('secondMap', new BsonArray([new BsonDateTime(3), new BsonDateTime(4)])) + ])) + + when: + reader.readStartDocument() + reader.readName('instants') + def decodedInstants = codec.decode(reader, DecoderContext.builder().build()) + + then: + decodedInstants == instants + } + + @SuppressWarnings('unused') + static class Container { + private final List>> instants = [] + + List>> getInstants() { + instants + } + } } diff --git a/bson/src/test/unit/org/bson/codecs/MapCodecSpecification.groovy b/bson/src/test/unit/org/bson/codecs/MapCodecSpecification.groovy index b91c60fe016..01eb70fddbf 100644 --- a/bson/src/test/unit/org/bson/codecs/MapCodecSpecification.groovy +++ b/bson/src/test/unit/org/bson/codecs/MapCodecSpecification.groovy @@ -16,8 +16,10 @@ package org.bson.codecs +import org.bson.BsonArray import org.bson.BsonBinaryReader import org.bson.BsonBinaryWriter +import org.bson.BsonDateTime import org.bson.BsonDbPointer import org.bson.BsonDocument import org.bson.BsonDocumentReader @@ -30,6 +32,7 @@ import org.bson.BsonUndefined import org.bson.BsonWriter import org.bson.ByteBufNIO import org.bson.Document +import org.bson.codecs.jsr310.Jsr310CodecProvider import org.bson.io.BasicOutputBuffer import org.bson.io.ByteBufferBsonInput import org.bson.json.JsonReader @@ -44,7 +47,9 @@ import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll +import java.lang.reflect.ParameterizedType import java.nio.ByteBuffer +import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -223,4 +228,46 @@ class MapCodecSpecification extends Specification { then: doc['_id'] == 5 } + + + def 'should parameterize'() { + given: + def codec = new MapCodec(REGISTRY, new BsonTypeClassMap()) + def writer = new BsonDocumentWriter(new BsonDocument()) + def reader = new BsonDocumentReader(writer.getDocument()) + def instants = + ['firstMap': [Instant.ofEpochMilli(1), Instant.ofEpochMilli(2)], + 'secondMap': [Instant.ofEpochMilli(3), Instant.ofEpochMilli(4)]] + when: + codec = codec.parameterize(fromProviders(new Jsr310CodecProvider(), REGISTRY), + asList(((ParameterizedType) Container.getMethod('getInstants').genericReturnType).actualTypeArguments)) + writer.writeStartDocument() + writer.writeName('instants') + codec.encode(writer, instants, EncoderContext.builder().build()) + writer.writeEndDocument() + + then: + writer.getDocument() == new BsonDocument() + .append('instants', + new BsonDocument() + .append('firstMap', new BsonArray([new BsonDateTime(1), new BsonDateTime(2)])) + .append('secondMap', new BsonArray([new BsonDateTime(3), new BsonDateTime(4)]))) + + when: + reader.readStartDocument() + reader.readName('instants') + def decodedInstants = codec.decode(reader, DecoderContext.builder().build()) + + then: + decodedInstants == instants + } + + @SuppressWarnings('unused') + static class Container { + private final Map> instants = [:] + + Map> getInstants() { + instants + } + } }