Skip to content

Commit a4e0a53

Browse files
committed
PojoCodec: Fix mapping for nested generic types
JAVA-3408
1 parent 716da74 commit a4e0a53

22 files changed

+1015
-118
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.bson.codecs.pojo;
18+
19+
import java.util.Objects;
20+
import java.util.function.Consumer;
21+
import java.util.function.Function;
22+
23+
import static org.bson.assertions.Assertions.notNull;
24+
25+
final class Either<L, R> {
26+
27+
public static <L, R> Either<L, R> left(final L value) {
28+
return new Either<>(notNull("value", value), null);
29+
}
30+
31+
public static <L, R> Either<L, R> right(final R value) {
32+
return new Either<>(null, notNull("value", value));
33+
}
34+
35+
private final L left;
36+
private final R right;
37+
38+
private Either(final L l, final R r) {
39+
left = l;
40+
right = r;
41+
}
42+
43+
public <T> T map(final Function<? super L, ? extends T> lFunc, final Function<? super R, ? extends T> rFunc) {
44+
return left != null ? lFunc.apply(left) : rFunc.apply(right);
45+
}
46+
47+
public void apply(final Consumer<? super L> lFunc, final Consumer<? super R> rFunc) {
48+
if (left != null){
49+
lFunc.accept(left);
50+
}
51+
if (right != null){
52+
rFunc.accept(right);
53+
}
54+
}
55+
56+
@Override
57+
public String toString() {
58+
return "Either{"
59+
+ "left=" + left
60+
+ ", right=" + right
61+
+ '}';
62+
}
63+
64+
@Override
65+
public boolean equals(final Object o) {
66+
if (this == o) {
67+
return true;
68+
}
69+
if (o == null || getClass() != o.getClass()) {
70+
return false;
71+
}
72+
73+
Either<?, ?> either = (Either<?, ?>) o;
74+
return Objects.equals(left, either.left) && Objects.equals(right, either.right);
75+
}
76+
77+
@Override
78+
public int hashCode() {
79+
return Objects.hash(left, right);
80+
}
81+
}

bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import static java.util.Arrays.asList;
3737
import static java.util.Collections.reverse;
3838
import static org.bson.assertions.Assertions.notNull;
39+
import static org.bson.codecs.pojo.PojoSpecializationHelper.specializeTypeData;
3940
import static org.bson.codecs.pojo.PropertyReflectionUtils.getPropertyMethods;
4041
import static org.bson.codecs.pojo.PropertyReflectionUtils.isGetter;
4142
import static org.bson.codecs.pojo.PropertyReflectionUtils.toPropertyName;
@@ -225,7 +226,8 @@ static <T> PropertyModelBuilder<T> createPropertyModelBuilder(final PropertyMeta
225226
.setError(propertyMetadata.getError());
226227

227228
if (propertyMetadata.getTypeParameters() != null) {
228-
specializePropertyModelBuilder(propertyModelBuilder, propertyMetadata);
229+
propertyModelBuilder.typeData(specializeTypeData(propertyModelBuilder.getTypeData(), propertyMetadata.getTypeParameters(),
230+
propertyMetadata.getTypeParameterMap()));
229231
}
230232

231233
return propertyModelBuilder;
@@ -243,37 +245,14 @@ private static TypeParameterMap getTypeParameterMap(final List<String> genericTy
243245
classParamIndex = genericTypeNames.indexOf(pt.getActualTypeArguments()[i].toString());
244246
if (classParamIndex != -1) {
245247
builder.addIndex(i, classParamIndex);
248+
} else {
249+
builder.addIndex(i, getTypeParameterMap(genericTypeNames, pt.getActualTypeArguments()[i]));
246250
}
247251
}
248252
}
249253
}
250254
return builder.build();
251255
}
252-
@SuppressWarnings("unchecked")
253-
private static <V> void specializePropertyModelBuilder(final PropertyModelBuilder<V> propertyModelBuilder,
254-
final PropertyMetadata<V> propertyMetadata) {
255-
if (propertyMetadata.getTypeParameterMap().hasTypeParameters() && !propertyMetadata.getTypeParameters().isEmpty()) {
256-
TypeData<V> specializedFieldType;
257-
Map<Integer, Integer> fieldToClassParamIndexMap = propertyMetadata.getTypeParameterMap().getPropertyToClassParamIndexMap();
258-
Integer classTypeParamRepresentsWholeField = fieldToClassParamIndexMap.get(-1);
259-
if (classTypeParamRepresentsWholeField != null) {
260-
specializedFieldType = (TypeData<V>) propertyMetadata.getTypeParameters().get(classTypeParamRepresentsWholeField);
261-
} else {
262-
TypeData.Builder<V> builder = TypeData.builder(propertyModelBuilder.getTypeData().getType());
263-
List<TypeData<?>> typeParameters = new ArrayList<TypeData<?>>(propertyModelBuilder.getTypeData().getTypeParameters());
264-
for (int i = 0; i < typeParameters.size(); i++) {
265-
for (Map.Entry<Integer, Integer> mapping : fieldToClassParamIndexMap.entrySet()) {
266-
if (mapping.getKey().equals(i)) {
267-
typeParameters.set(i, propertyMetadata.getTypeParameters().get(mapping.getValue()));
268-
}
269-
}
270-
}
271-
builder.addTypeParameters(typeParameters);
272-
specializedFieldType = builder.build();
273-
}
274-
propertyModelBuilder.typeData(specializedFieldType);
275-
}
276-
}
277256

278257
static <V> V stateNotNull(final String property, final V value) {
279258
if (value == null) {

bson/src/main/org/bson/codecs/pojo/PojoCodecImpl.java

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import static java.lang.String.format;
3939
import static org.bson.codecs.configuration.CodecRegistries.fromCodecs;
4040
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
41+
import static org.bson.codecs.pojo.PojoSpecializationHelper.specializeTypeData;
4142

4243

4344
final class PojoCodecImpl<T> extends PojoCodec<T> {
@@ -293,7 +294,7 @@ private <S, V> ClassModel<S> getSpecializedClassModel(final ClassModel<S> clazzM
293294
String propertyName = model.getName();
294295
TypeParameterMap typeParameterMap = clazzModel.getPropertyNameToTypeParameterMap().get(propertyName);
295296
if (typeParameterMap.hasTypeParameters()) {
296-
PropertyModel<?> concretePropertyModel = getSpecializedPropertyModel(model, typeParameterMap, propertyTypeParameters);
297+
PropertyModel<?> concretePropertyModel = getSpecializedPropertyModel(model, propertyTypeParameters, typeParameterMap);
297298
concretePropertyModels.set(i, concretePropertyModel);
298299
if (concreteIdProperty != null && concreteIdProperty.getName().equals(propertyName)) {
299300
concreteIdProperty = concretePropertyModel;
@@ -308,26 +309,10 @@ private <S, V> ClassModel<S> getSpecializedClassModel(final ClassModel<S> clazzM
308309
}
309310

310311
@SuppressWarnings("unchecked")
311-
private <V> PropertyModel<V> getSpecializedPropertyModel(final PropertyModel<V> propertyModel, final TypeParameterMap typeParameterMap,
312-
final List<TypeData<?>> propertyTypeParameters) {
313-
TypeData<V> specializedPropertyType;
314-
Map<Integer, Integer> propertyToClassParamIndexMap = typeParameterMap.getPropertyToClassParamIndexMap();
315-
Integer classTypeParamRepresentsWholeProperty = propertyToClassParamIndexMap.get(-1);
316-
if (classTypeParamRepresentsWholeProperty != null) {
317-
specializedPropertyType = (TypeData<V>) propertyTypeParameters.get(classTypeParamRepresentsWholeProperty);
318-
} else {
319-
TypeData.Builder<V> builder = TypeData.builder(propertyModel.getTypeData().getType());
320-
List<TypeData<?>> typeParameters = new ArrayList<TypeData<?>>(propertyModel.getTypeData().getTypeParameters());
321-
for (int i = 0; i < typeParameters.size(); i++) {
322-
for (Map.Entry<Integer, Integer> mapping : propertyToClassParamIndexMap.entrySet()) {
323-
if (mapping.getKey().equals(i)) {
324-
typeParameters.set(i, propertyTypeParameters.get(mapping.getValue()));
325-
}
326-
}
327-
}
328-
builder.addTypeParameters(typeParameters);
329-
specializedPropertyType = builder.build();
330-
}
312+
private <V> PropertyModel<V> getSpecializedPropertyModel(final PropertyModel<V> propertyModel,
313+
final List<TypeData<?>> propertyTypeParameters,
314+
final TypeParameterMap typeParameterMap) {
315+
TypeData<V> specializedPropertyType = specializeTypeData(propertyModel.getTypeData(), propertyTypeParameters, typeParameterMap);
331316
if (propertyModel.getTypeData().equals(specializedPropertyType)) {
332317
return propertyModel;
333318
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.bson.codecs.pojo;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
final class PojoSpecializationHelper {
24+
25+
@SuppressWarnings("unchecked")
26+
static <V> TypeData<V> specializeTypeData(final TypeData<V> typeData, final List<TypeData<?>> typeParameters,
27+
final TypeParameterMap typeParameterMap) {
28+
if (!typeParameterMap.hasTypeParameters() || typeParameters.isEmpty()) {
29+
return typeData;
30+
}
31+
32+
Map<Integer, Either<Integer, TypeParameterMap>> propertyToClassParamIndexMap = typeParameterMap.getPropertyToClassParamIndexMap();
33+
Either<Integer, TypeParameterMap> classTypeParamRepresentsWholeField = propertyToClassParamIndexMap.get(-1);
34+
if (classTypeParamRepresentsWholeField != null) {
35+
Integer index = classTypeParamRepresentsWholeField.map(i -> i, e -> {
36+
throw new IllegalStateException("Invalid state, the whole class cannot be represented by a subtype.");
37+
});
38+
return (TypeData<V>) typeParameters.get(index);
39+
} else {
40+
return getTypeData(typeData, typeParameters, propertyToClassParamIndexMap);
41+
}
42+
}
43+
44+
private static <V> TypeData<V> getTypeData(final TypeData<V> typeData, final List<TypeData<?>> specializedTypeParameters,
45+
final Map<Integer, Either<Integer, TypeParameterMap>> propertyToClassParamIndexMap) {
46+
List<TypeData<?>> subTypeParameters = new ArrayList<>(typeData.getTypeParameters());
47+
for (int i = 0; i < typeData.getTypeParameters().size(); i++) {
48+
subTypeParameters.set(i, getTypeData(subTypeParameters.get(i), specializedTypeParameters, propertyToClassParamIndexMap, i));
49+
}
50+
return TypeData.builder(typeData.getType()).addTypeParameters(subTypeParameters).build();
51+
}
52+
53+
private static TypeData<?> getTypeData(final TypeData<?> typeData, final List<TypeData<?>> specializedTypeParameters,
54+
final Map<Integer, Either<Integer, TypeParameterMap>> propertyToClassParamIndexMap,
55+
final int index) {
56+
if (!propertyToClassParamIndexMap.containsKey(index)) {
57+
return typeData;
58+
}
59+
return propertyToClassParamIndexMap.get(index).map(l -> {
60+
if (typeData.getTypeParameters().isEmpty()) {
61+
// Represents the whole typeData
62+
return specializedTypeParameters.get(l);
63+
} else {
64+
// Represents a single nested type parameter within this typeData
65+
TypeData.Builder<?> builder = TypeData.builder(typeData.getType());
66+
List<TypeData<?>> typeParameters = new ArrayList<>(typeData.getTypeParameters());
67+
typeParameters.set(index, specializedTypeParameters.get(l));
68+
builder.addTypeParameters(typeParameters);
69+
return builder.build();
70+
}
71+
},
72+
r -> {
73+
// Represents a child type parameter of this typeData
74+
return getTypeData(typeData, specializedTypeParameters, r.getPropertyToClassParamIndexMap());
75+
});
76+
}
77+
78+
private PojoSpecializationHelper() {
79+
}
80+
}

bson/src/main/org/bson/codecs/pojo/PropertyMetadata.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,21 @@ private boolean notStaticOrTransient(final int modifiers) {
168168
private boolean isPublicAndNotStaticOrTransient(final int modifiers) {
169169
return isPublic(modifiers) && notStaticOrTransient(modifiers);
170170
}
171+
172+
@Override
173+
public String toString() {
174+
return "PropertyMetadata{"
175+
+ "name='" + name + '\''
176+
+ ", declaringClassName='" + declaringClassName + '\''
177+
+ ", typeData=" + typeData
178+
+ ", readAnnotations=" + readAnnotations
179+
+ ", writeAnnotations=" + writeAnnotations
180+
+ ", typeParameterMap=" + typeParameterMap
181+
+ ", typeParameters=" + typeParameters
182+
+ ", error='" + error + '\''
183+
+ ", field=" + field
184+
+ ", getter=" + getter
185+
+ ", setter=" + setter
186+
+ '}';
187+
}
171188
}

bson/src/main/org/bson/codecs/pojo/TypeParameterMap.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* Maps the index of a class's generic parameter type index to a property's.
2727
*/
2828
final class TypeParameterMap {
29-
private final Map<Integer, Integer> propertyToClassParamIndexMap;
29+
private final Map<Integer, Either<Integer, TypeParameterMap>> propertyToClassParamIndexMap;
3030

3131
/**
3232
* Creates a new builder for the TypeParameterMap
@@ -44,7 +44,7 @@ static Builder builder() {
4444
*
4545
* @return a mapping of property type parameter index to the class type parameter index.
4646
*/
47-
Map<Integer, Integer> getPropertyToClassParamIndexMap() {
47+
Map<Integer, Either<Integer, TypeParameterMap>> getPropertyToClassParamIndexMap() {
4848
return propertyToClassParamIndexMap;
4949
}
5050

@@ -56,7 +56,7 @@ boolean hasTypeParameters() {
5656
* A builder for mapping field type parameter indices to the class type parameter indices
5757
*/
5858
static final class Builder {
59-
private final Map<Integer, Integer> propertyToClassParamIndexMap = new HashMap<Integer, Integer>();
59+
private final Map<Integer, Either<Integer, TypeParameterMap>> propertyToClassParamIndexMap = new HashMap<>();
6060

6161
private Builder() {
6262
}
@@ -68,7 +68,7 @@ private Builder() {
6868
* @return this
6969
*/
7070
Builder addIndex(final int classTypeParameterIndex) {
71-
propertyToClassParamIndexMap.put(-1, classTypeParameterIndex);
71+
propertyToClassParamIndexMap.put(-1, Either.left(classTypeParameterIndex));
7272
return this;
7373
}
7474

@@ -80,7 +80,20 @@ Builder addIndex(final int classTypeParameterIndex) {
8080
* @return this
8181
*/
8282
Builder addIndex(final int propertyTypeParameterIndex, final int classTypeParameterIndex) {
83-
propertyToClassParamIndexMap.put(propertyTypeParameterIndex, classTypeParameterIndex);
83+
propertyToClassParamIndexMap.put(propertyTypeParameterIndex, Either.left(classTypeParameterIndex));
84+
return this;
85+
}
86+
87+
88+
/**
89+
* Adds a mapping that represents the property
90+
*
91+
* @param propertyTypeParameterIndex the property's type parameter index
92+
* @param typeParameterMap the sub class's type parameter map
93+
* @return this
94+
*/
95+
Builder addIndex(final int propertyTypeParameterIndex, final TypeParameterMap typeParameterMap) {
96+
propertyToClassParamIndexMap.put(propertyTypeParameterIndex, Either.right(typeParameterMap));
8497
return this;
8598
}
8699

@@ -125,7 +138,7 @@ public int hashCode() {
125138
return getPropertyToClassParamIndexMap().hashCode();
126139
}
127140

128-
private TypeParameterMap(final Map<Integer, Integer> propertyToClassParamIndexMap) {
141+
private TypeParameterMap(final Map<Integer, Either<Integer, TypeParameterMap>> propertyToClassParamIndexMap) {
129142
this.propertyToClassParamIndexMap = unmodifiableMap(propertyToClassParamIndexMap);
130143
}
131144
}

bson/src/test/unit/org/bson/codecs/pojo/ClassModelBuilderTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public void testDefaults() {
5858
fieldNameToTypeParameterMap.put("myIntegerField", TypeParameterMap.builder().build());
5959
fieldNameToTypeParameterMap.put("myGenericField", TypeParameterMap.builder().addIndex(0).build());
6060
fieldNameToTypeParameterMap.put("myListField", TypeParameterMap.builder().addIndex(0, 1).build());
61-
fieldNameToTypeParameterMap.put("myMapField", TypeParameterMap.builder().addIndex(1, 2).build());
61+
fieldNameToTypeParameterMap.put("myMapField", TypeParameterMap.builder().addIndex(0, TypeParameterMap.builder().build())
62+
.addIndex(1, 2).build());
6263

6364
assertEquals(fieldNameToTypeParameterMap, builder.getPropertyNameToTypeParameterMap());
6465
assertEquals(3, builder.getConventions().size());

0 commit comments

Comments
 (0)