Skip to content

Commit cf2a6a3

Browse files
committed
Added the USE_GETTERS_FOR_SETTERS convention
Allows the getters for Map / Collection properties to provide the initial data, which is then mutated. JAVA-2648
1 parent cb20df6 commit cf2a6a3

13 files changed

+661
-0
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2017 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 org.bson.codecs.configuration.CodecConfigurationException;
20+
21+
import java.util.Collection;
22+
import java.util.Map;
23+
24+
import static java.lang.String.format;
25+
26+
final class ConventionUseGettersAsSettersImpl implements Convention {
27+
28+
@Override
29+
public void apply(final ClassModelBuilder<?> classModelBuilder) {
30+
for (PropertyModelBuilder<?> propertyModelBuilder : classModelBuilder.getPropertyModelBuilders()) {
31+
if (!(propertyModelBuilder.getPropertyAccessor() instanceof PropertyAccessorImpl)) {
32+
throw new CodecConfigurationException(format("The USE_GETTER_AS_SETTER_CONVENTION is not compatible with "
33+
+ "propertyModelBuilder instance that have custom implementations of org.bson.codecs.pojo.PropertyAccessor: %s",
34+
propertyModelBuilder.getPropertyAccessor().getClass().getName()));
35+
}
36+
PropertyAccessorImpl<?> defaultAccessor = (PropertyAccessorImpl<?>) propertyModelBuilder.getPropertyAccessor();
37+
PropertyMetadata<?> propertyMetaData = defaultAccessor.getPropertyMetadata();
38+
if (!propertyMetaData.isDeserializable() && propertyMetaData.isSerializable()
39+
&& isMapOrCollection(propertyMetaData.getTypeData().getType())) {
40+
setPropertyAccessor(propertyModelBuilder);
41+
}
42+
}
43+
}
44+
45+
private <T> boolean isMapOrCollection(final Class<T> clazz) {
46+
return Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz);
47+
}
48+
49+
@SuppressWarnings("unchecked")
50+
private <T> void setPropertyAccessor(final PropertyModelBuilder<T> propertyModelBuilder) {
51+
propertyModelBuilder.propertyAccessor(new PrivateProperyAccessor<T>(
52+
(PropertyAccessorImpl<T>) propertyModelBuilder.getPropertyAccessor()));
53+
}
54+
55+
@SuppressWarnings({"rawtypes", "unchecked"})
56+
private static final class PrivateProperyAccessor<T> implements PropertyAccessor<T> {
57+
private final PropertyAccessorImpl<T> wrapped;
58+
59+
private PrivateProperyAccessor(final PropertyAccessorImpl<T> wrapped) {
60+
this.wrapped = wrapped;
61+
}
62+
63+
@Override
64+
public <S> T get(final S instance) {
65+
return wrapped.get(instance);
66+
}
67+
68+
@Override
69+
public <S> void set(final S instance, final T value) {
70+
if (value instanceof Collection) {
71+
mutateCollection(instance, (Collection) value);
72+
} else if (value instanceof Map) {
73+
mutateMap(instance, (Map) value);
74+
} else {
75+
throwCodecConfigurationException(format("Unexpected type: '%s'", value.getClass()), null);
76+
}
77+
}
78+
79+
private <S> void mutateCollection(final S instance, final Collection value) {
80+
T originalCollection = get(instance);
81+
Collection<?> collection = ((Collection<?>) originalCollection);
82+
if (collection == null) {
83+
throwCodecConfigurationException("The getter returned null.", null);
84+
} else if (!collection.isEmpty()) {
85+
throwCodecConfigurationException("The getter returned a non empty collection.", null);
86+
} else {
87+
try {
88+
collection.addAll(value);
89+
} catch (Exception e) {
90+
throwCodecConfigurationException("collection#addAll failed.", e);
91+
}
92+
}
93+
}
94+
95+
private <S> void mutateMap(final S instance, final Map value) {
96+
T originalMap = get(instance);
97+
Map<?, ?> map = ((Map<?, ?>) originalMap);
98+
if (map == null) {
99+
throwCodecConfigurationException("The getter returned null.", null);
100+
} else if (!map.isEmpty()) {
101+
throwCodecConfigurationException("The getter returned a non empty map.", null);
102+
} else {
103+
try {
104+
map.putAll(value);
105+
} catch (Exception e) {
106+
throwCodecConfigurationException("map#putAll failed.", e);
107+
}
108+
}
109+
}
110+
private void throwCodecConfigurationException(final String reason, final Exception cause) {
111+
throw new CodecConfigurationException(format("Cannot use getter in '%s' to set '%s'. %s",
112+
wrapped.getPropertyMetadata().getDeclaringClassName(), wrapped.getPropertyMetadata().getName(), reason), cause);
113+
}
114+
}
115+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ public final class Conventions {
5959
*/
6060
public static final Convention SET_PRIVATE_FIELDS_CONVENTION = new ConventionSetPrivateFieldImpl();
6161

62+
/**
63+
* A convention that uses getter methods as setters for collections and maps if there is no setter.
64+
*
65+
* <p>This convention mimics how JAXB mutate collections and maps.</p>
66+
* <p>Note: This convention is not part of the {@code DEFAULT_CONVENTIONS} list and must explicitly be set.</p>
67+
*
68+
* @since 3.6
69+
*/
70+
public static final Convention USE_GETTERS_FOR_SETTERS = new ConventionUseGettersAsSettersImpl();
71+
6272
/**
6373
* The default conventions list
6474
*/

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,17 @@
4343
import org.bson.codecs.pojo.entities.SimpleNestedPojoModel;
4444
import org.bson.codecs.pojo.entities.UpperBoundsModel;
4545
import org.bson.codecs.pojo.entities.conventions.AnnotationModel;
46+
import org.bson.codecs.pojo.entities.conventions.CollectionsGetterNonEmptyModel;
47+
import org.bson.codecs.pojo.entities.conventions.CollectionsGetterNullModel;
48+
import org.bson.codecs.pojo.entities.conventions.CollectionsGetterImmutableModel;
49+
import org.bson.codecs.pojo.entities.conventions.CollectionsGetterMutableModel;
4650
import org.bson.codecs.pojo.entities.conventions.CreatorConstructorPrimitivesModel;
4751
import org.bson.codecs.pojo.entities.conventions.CreatorConstructorThrowsExceptionModel;
4852
import org.bson.codecs.pojo.entities.conventions.CreatorMethodThrowsExceptionModel;
53+
import org.bson.codecs.pojo.entities.conventions.MapGetterNonEmptyModel;
54+
import org.bson.codecs.pojo.entities.conventions.MapGetterNullModel;
55+
import org.bson.codecs.pojo.entities.conventions.MapGetterImmutableModel;
56+
import org.bson.codecs.pojo.entities.conventions.MapGetterMutableModel;
4957
import org.bson.types.ObjectId;
5058
import org.junit.Test;
5159

@@ -63,6 +71,7 @@
6371
import static org.bson.codecs.pojo.Conventions.DEFAULT_CONVENTIONS;
6472
import static org.bson.codecs.pojo.Conventions.NO_CONVENTIONS;
6573
import static org.bson.codecs.pojo.Conventions.SET_PRIVATE_FIELDS_CONVENTION;
74+
import static org.bson.codecs.pojo.Conventions.USE_GETTERS_FOR_SETTERS;
6675

6776
public final class PojoCustomTest extends PojoTestCase {
6877

@@ -168,6 +177,79 @@ public void testSetPrivateFieldConvention() {
168177
"{'integerField': 1, 'stringField': '2', listField: ['a', 'b']}");
169178
}
170179

180+
@Test
181+
public void testUseGettersForSettersConvention() {
182+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(CollectionsGetterMutableModel.class, MapGetterMutableModel.class)
183+
.conventions(getDefaultAndUseGettersConvention());
184+
185+
roundTrip(builder, new CollectionsGetterMutableModel(asList(1, 2)), "{listField: [1, 2]}");
186+
roundTrip(builder, new MapGetterMutableModel(Collections.singletonMap("a", 3)), "{mapField: {a: 3}}");
187+
}
188+
189+
@Test(expected = CodecConfigurationException.class)
190+
public void testUseGettersForSettersConventionInvalidTypeForCollection() {
191+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(CollectionsGetterMutableModel.class)
192+
.conventions(getDefaultAndUseGettersConvention());
193+
194+
decodingShouldFail(getCodec(builder, CollectionsGetterMutableModel.class), "{listField: ['1', '2']}");
195+
}
196+
197+
@Test(expected = CodecConfigurationException.class)
198+
public void testUseGettersForSettersConventionInvalidTypeForMap() {
199+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(MapGetterMutableModel.class)
200+
.conventions(getDefaultAndUseGettersConvention());
201+
202+
decodingShouldFail(getCodec(builder, MapGetterMutableModel.class), "{mapField: {a: '1'}}");
203+
}
204+
205+
@Test(expected = CodecConfigurationException.class)
206+
public void testUseGettersForSettersConventionImmutableCollection() {
207+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(CollectionsGetterImmutableModel.class)
208+
.conventions(getDefaultAndUseGettersConvention());
209+
210+
roundTrip(builder, new CollectionsGetterImmutableModel(asList(1, 2)), "{listField: [1, 2]}");
211+
}
212+
213+
@Test(expected = CodecConfigurationException.class)
214+
public void testUseGettersForSettersConventionImmutableMap() {
215+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(MapGetterImmutableModel.class)
216+
.conventions(getDefaultAndUseGettersConvention());
217+
218+
roundTrip(builder, new MapGetterImmutableModel(Collections.singletonMap("a", 3)), "{mapField: {a: 3}}");
219+
}
220+
221+
@Test(expected = CodecConfigurationException.class)
222+
public void testUseGettersForSettersConventionNullCollection() {
223+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(CollectionsGetterNullModel.class)
224+
.conventions(getDefaultAndUseGettersConvention());
225+
226+
roundTrip(builder, new CollectionsGetterNullModel(asList(1, 2)), "{listField: [1, 2]}");
227+
}
228+
229+
@Test(expected = CodecConfigurationException.class)
230+
public void testUseGettersForSettersConventionNullMap() {
231+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(MapGetterNullModel.class)
232+
.conventions(getDefaultAndUseGettersConvention());
233+
234+
roundTrip(builder, new MapGetterNullModel(Collections.singletonMap("a", 3)), "{mapField: {a: 3}}");
235+
}
236+
237+
@Test(expected = CodecConfigurationException.class)
238+
public void testUseGettersForSettersConventionNotEmptyCollection() {
239+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(CollectionsGetterNonEmptyModel.class)
240+
.conventions(getDefaultAndUseGettersConvention());
241+
242+
roundTrip(builder, new CollectionsGetterNonEmptyModel(asList(1, 2)), "{listField: [1, 2]}");
243+
}
244+
245+
@Test(expected = CodecConfigurationException.class)
246+
public void testUseGettersForSettersConventionNotEmptyMap() {
247+
PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(MapGetterNonEmptyModel.class)
248+
.conventions(getDefaultAndUseGettersConvention());
249+
250+
roundTrip(builder, new MapGetterNonEmptyModel(Collections.singletonMap("a", 3)), "{mapField: {a: 3}}");
251+
}
252+
171253
@Test
172254
public void testEnumSupport() {
173255
roundTrip(getPojoCodecProviderBuilder(SimpleEnumModel.class), new SimpleEnumModel(SimpleEnum.BRAVO), "{ 'myEnum': 'BRAVO' }");
@@ -402,4 +484,11 @@ public void testInvalidGetterAndSetterModelEncoding() {
402484
public void testInvalidGetterAndSetterModelDecoding() {
403485
decodingShouldFail(getCodec(InvalidGetterAndSetterModel.class), "{'integerField': 42, 'stringField': 'myString'}");
404486
}
487+
488+
private List<Convention> getDefaultAndUseGettersConvention() {
489+
List<Convention> conventions = new ArrayList<Convention>(DEFAULT_CONVENTIONS);
490+
conventions.add(USE_GETTERS_FOR_SETTERS);
491+
return conventions;
492+
}
493+
405494
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2017 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 java.util.Collections;
20+
import java.util.List;
21+
22+
public class CollectionsGetterImmutableModel {
23+
24+
private final List<Integer> listField;
25+
26+
public CollectionsGetterImmutableModel() {
27+
this(Collections.<Integer>emptyList());
28+
}
29+
30+
public CollectionsGetterImmutableModel(final List<Integer> listField) {
31+
this.listField = Collections.unmodifiableList(listField);
32+
}
33+
34+
public List<Integer> getListField() {
35+
return listField;
36+
}
37+
38+
@Override
39+
public boolean equals(final Object o) {
40+
if (this == o) {
41+
return true;
42+
}
43+
if (o == null || getClass() != o.getClass()){
44+
return false;
45+
}
46+
47+
CollectionsGetterImmutableModel that = (CollectionsGetterImmutableModel) o;
48+
49+
return listField != null ? listField.equals(that.listField) : that.listField == null;
50+
}
51+
52+
@Override
53+
public int hashCode() {
54+
return listField != null ? listField.hashCode() : 0;
55+
}
56+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2017 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 java.util.ArrayList;
20+
import java.util.List;
21+
22+
public class CollectionsGetterMutableModel {
23+
24+
private final List<Integer> listField;
25+
26+
public CollectionsGetterMutableModel() {
27+
this(new ArrayList<Integer>());
28+
}
29+
30+
public CollectionsGetterMutableModel(final List<Integer> listField) {
31+
this.listField = listField;
32+
}
33+
34+
public List<Integer> getListField() {
35+
return listField;
36+
}
37+
38+
@Override
39+
public boolean equals(final Object o) {
40+
if (this == o) {
41+
return true;
42+
}
43+
if (o == null || getClass() != o.getClass()) {
44+
return false;
45+
}
46+
47+
CollectionsGetterMutableModel that = (CollectionsGetterMutableModel) o;
48+
return listField != null ? listField.equals(that.listField) : that.listField == null;
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return listField != null ? listField.hashCode() : 0;
54+
}
55+
}

0 commit comments

Comments
 (0)