Skip to content

Commit 33719ef

Browse files
jflorenciorozza
andauthored
JAVA-3815: Pojo Codec - Detect property models on extended interfaces (#563)
Improve Pojo Codecs usability by detecting property models on extended interfaces and allow for instantiation without the need for custom `@BsonCreator`. JAVA-3815 --------- Co-authored-by: Ross Lawley <[email protected]>
1 parent 6dd96da commit 33719ef

File tree

5 files changed

+183
-11
lines changed

5 files changed

+183
-11
lines changed

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.lang.reflect.TypeVariable;
2626
import java.util.ArrayList;
2727
import java.util.HashMap;
28+
import java.util.LinkedHashSet;
2829
import java.util.List;
2930
import java.util.Map;
3031
import java.util.Set;
@@ -50,12 +51,12 @@ static <T> void configureClassModelBuilder(final ClassModelBuilder<T> classModel
5051
ArrayList<Annotation> annotations = new ArrayList<>();
5152
Set<String> propertyNames = new TreeSet<>();
5253
Map<String, TypeParameterMap> propertyTypeParameterMap = new HashMap<>();
53-
Class<? super T> currentClass = clazz;
5454
String declaringClassName = clazz.getSimpleName();
55-
TypeData<?> parentClassTypeData = null;
5655

5756
Map<String, PropertyMetadata<?>> propertyNameMap = new HashMap<>();
58-
while (!currentClass.isEnum() && currentClass.getSuperclass() != null) {
57+
for (ClassWithParentTypeData<? super T> currentClassWithParentTypeData : getClassHierarchy(clazz, null)) {
58+
Class<? super T> currentClass = currentClassWithParentTypeData.clazz;
59+
TypeData<?> parentClassTypeData = currentClassWithParentTypeData.parentClassTypeData;
5960
annotations.addAll(asList(currentClass.getDeclaredAnnotations()));
6061
List<String> genericTypeNames = new ArrayList<>();
6162
for (TypeVariable<? extends Class<? super T>> classTypeVariable : currentClass.getTypeParameters()) {
@@ -116,13 +117,6 @@ static <T> void configureClassModelBuilder(final ClassModelBuilder<T> classModel
116117
}
117118
}
118119
}
119-
120-
parentClassTypeData = TypeData.newInstance(currentClass.getGenericSuperclass(), currentClass);
121-
currentClass = currentClass.getSuperclass();
122-
}
123-
124-
if (currentClass.isInterface()) {
125-
annotations.addAll(asList(currentClass.getDeclaredAnnotations()));
126120
}
127121

128122
for (String propertyName : propertyNames) {
@@ -262,6 +256,33 @@ static <V> V stateNotNull(final String property, final V value) {
262256
return value;
263257
}
264258

259+
@SuppressWarnings("unchecked")
260+
private static <T> Set<ClassWithParentTypeData<? super T>> getClassHierarchy(final Class<? super T> clazz,
261+
final TypeData<?> classTypeData) {
262+
Set<ClassWithParentTypeData<? super T>> classesToScan = new LinkedHashSet<>();
263+
Class<? super T> currentClass = clazz;
264+
TypeData<?> parentClassTypeData = classTypeData;
265+
while (currentClass != null && !currentClass.isEnum() && !currentClass.equals(Object.class)) {
266+
classesToScan.add(new ClassWithParentTypeData<>(currentClass, parentClassTypeData));
267+
parentClassTypeData = TypeData.newInstance(currentClass.getGenericSuperclass(), currentClass);
268+
for (Class<?> interfaceClass : currentClass.getInterfaces()) {
269+
classesToScan.addAll(getClassHierarchy((Class<? super T>) interfaceClass, parentClassTypeData));
270+
}
271+
currentClass = currentClass.getSuperclass();
272+
}
273+
return classesToScan;
274+
}
275+
276+
private static final class ClassWithParentTypeData<T> {
277+
private final Class<T> clazz;
278+
private final TypeData<?> parentClassTypeData;
279+
280+
private ClassWithParentTypeData(final Class<T> clazz, final TypeData<?> parentClassTypeData) {
281+
this.clazz = clazz;
282+
this.parentClassTypeData = parentClassTypeData;
283+
}
284+
}
285+
265286
private PojoBuilderHelper() {
266287
}
267288
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.bson.codecs.pojo.entities.AsymmetricalModel;
3737
import org.bson.codecs.pojo.entities.BsonRepresentationUnsupportedInt;
3838
import org.bson.codecs.pojo.entities.BsonRepresentationUnsupportedString;
39+
import org.bson.codecs.pojo.entities.ComposeInterfaceModel;
3940
import org.bson.codecs.pojo.entities.ConcreteAndNestedAbstractInterfaceModel;
4041
import org.bson.codecs.pojo.entities.ConcreteCollectionsModel;
4142
import org.bson.codecs.pojo.entities.ConcreteModel;
@@ -49,6 +50,8 @@
4950
import org.bson.codecs.pojo.entities.GenericHolderModel;
5051
import org.bson.codecs.pojo.entities.GenericTreeModel;
5152
import org.bson.codecs.pojo.entities.InterfaceBasedModel;
53+
import org.bson.codecs.pojo.entities.InterfaceModelB;
54+
import org.bson.codecs.pojo.entities.InterfaceModelImpl;
5255
import org.bson.codecs.pojo.entities.InvalidCollectionModel;
5356
import org.bson.codecs.pojo.entities.InvalidGetterAndSetterModel;
5457
import org.bson.codecs.pojo.entities.InvalidMapModel;
@@ -79,6 +82,7 @@
7982
import org.bson.codecs.pojo.entities.conventions.CreatorConstructorPrimitivesModel;
8083
import org.bson.codecs.pojo.entities.conventions.CreatorConstructorThrowsExceptionModel;
8184
import org.bson.codecs.pojo.entities.conventions.CreatorMethodThrowsExceptionModel;
85+
import org.bson.codecs.pojo.entities.conventions.InterfaceModelBInstanceCreatorConvention;
8286
import org.bson.codecs.pojo.entities.conventions.MapGetterImmutableModel;
8387
import org.bson.codecs.pojo.entities.conventions.MapGetterMutableModel;
8488
import org.bson.codecs.pojo.entities.conventions.MapGetterNonEmptyModel;
@@ -509,6 +513,17 @@ public void testInvalidMapModelWithCustomPropertyCodecProvider() {
509513
"{'invalidMap': {'1': 1, '2': 2}}");
510514
}
511515

516+
@Test
517+
public void testInterfaceModelCreatorMadeInConvention() {
518+
roundTrip(
519+
getPojoCodecProviderBuilder(ComposeInterfaceModel.class, InterfaceModelB.class, InterfaceModelImpl.class)
520+
.conventions(Collections.singletonList(new InterfaceModelBInstanceCreatorConvention())),
521+
new ComposeInterfaceModel("someTitle",
522+
new InterfaceModelImpl("a", "b")),
523+
"{'title': 'someTitle', 'nestedModel': {'propertyA': 'a', 'propertyB': 'b'}}"
524+
);
525+
}
526+
512527
@Test
513528
public void testConstructorNotPublicModel() {
514529
assertThrows(CodecConfigurationException.class, () ->
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.entities;
18+
19+
import java.util.Objects;
20+
21+
public class ComposeInterfaceModel {
22+
private String title;
23+
private InterfaceModelB nestedModel;
24+
25+
public ComposeInterfaceModel() {
26+
}
27+
28+
public ComposeInterfaceModel(final String title, final InterfaceModelB nestedModel) {
29+
this.title = title;
30+
this.nestedModel = nestedModel;
31+
}
32+
33+
public String getTitle() {
34+
return title;
35+
}
36+
37+
public void setTitle(final String title) {
38+
this.title = title;
39+
}
40+
41+
public InterfaceModelB getNestedModel() {
42+
return nestedModel;
43+
}
44+
45+
public void setNestedModel(final InterfaceModelB nestedModel) {
46+
this.nestedModel = nestedModel;
47+
}
48+
49+
@Override
50+
public boolean equals(final Object o) {
51+
if (this == o) {
52+
return true;
53+
}
54+
if (o == null || getClass() != o.getClass()) {
55+
return false;
56+
}
57+
ComposeInterfaceModel that = (ComposeInterfaceModel) o;
58+
return Objects.equals(title, that.title)
59+
&& Objects.equals(nestedModel, that.nestedModel);
60+
}
61+
62+
@Override
63+
public int hashCode() {
64+
return Objects.hash(title, nestedModel);
65+
}
66+
67+
@Override
68+
public String toString() {
69+
return "ComposeInterfaceModel{"
70+
+ "title='" + title + '\''
71+
+ ", nestedModel=" + nestedModel
72+
+ '}';
73+
}
74+
}

bson/src/test/unit/org/bson/codecs/pojo/entities/InterfaceModelImpl.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,15 @@ public boolean equals(final Object o) {
6363
@Override
6464
public int hashCode() {
6565
int result = getPropertyA() != null ? getPropertyA().hashCode() : 0;
66-
result = 31 * result + getPropertyB() != null ? getPropertyB().hashCode() : 0;
66+
result = 31 * result + (getPropertyB() != null ? getPropertyB().hashCode() : 0);
6767
return result;
6868
}
69+
70+
@Override
71+
public String toString() {
72+
return "InterfaceModelImpl{"
73+
+ "propertyA='" + getPropertyA() + "', "
74+
+ "propertyB='" + getPropertyB() + '\''
75+
+ '}';
76+
}
6977
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.entities.conventions;
18+
19+
import org.bson.codecs.pojo.ClassModelBuilder;
20+
import org.bson.codecs.pojo.Convention;
21+
import org.bson.codecs.pojo.InstanceCreator;
22+
import org.bson.codecs.pojo.PropertyModel;
23+
import org.bson.codecs.pojo.entities.InterfaceModelB;
24+
import org.bson.codecs.pojo.entities.InterfaceModelImpl;
25+
26+
public class InterfaceModelBInstanceCreatorConvention implements Convention {
27+
@Override
28+
@SuppressWarnings("unchecked")
29+
public void apply(final ClassModelBuilder<?> classModelBuilder) {
30+
if (classModelBuilder.getType().equals(InterfaceModelB.class)) {
31+
// Simulate a custom implementation of InstanceCreator factory
32+
// (This one can be generated automatically, but, a real use case can have an advanced reflection based
33+
// solution that the POJO Codec doesn't support out of the box)
34+
((ClassModelBuilder<InterfaceModelB>) classModelBuilder).instanceCreatorFactory(() -> {
35+
InterfaceModelB interfaceModelB = new InterfaceModelImpl();
36+
return new InstanceCreator<InterfaceModelB>() {
37+
@Override
38+
public <S> void set(final S value, final PropertyModel<S> propertyModel) {
39+
if (propertyModel.getName().equals("propertyA")) {
40+
interfaceModelB.setPropertyA((String) value);
41+
} else if (propertyModel.getName().equals("propertyB")) {
42+
interfaceModelB.setPropertyB((String) value);
43+
}
44+
}
45+
46+
@Override
47+
public InterfaceModelB getInstance() {
48+
return interfaceModelB;
49+
}
50+
};
51+
});
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)