Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitae65b76

Browse files
committed
Added a semi deterministic object mapper
1 parentace75e4 commitae65b76

File tree

2 files changed

+312
-0
lines changed

2 files changed

+312
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
packagecom.stubbornjava.common;
2+
3+
importjava.util.Collection;
4+
importjava.util.Collections;
5+
importjava.util.Comparator;
6+
importjava.util.LinkedHashMap;
7+
importjava.util.Map.Entry;
8+
importjava.util.Objects;
9+
10+
importorg.jooq.lambda.Seq;
11+
12+
importcom.fasterxml.jackson.databind.BeanProperty;
13+
importcom.fasterxml.jackson.databind.JavaType;
14+
importcom.fasterxml.jackson.databind.JsonMappingException;
15+
importcom.fasterxml.jackson.databind.JsonSerializer;
16+
importcom.fasterxml.jackson.databind.MapperFeature;
17+
importcom.fasterxml.jackson.databind.ObjectMapper;
18+
importcom.fasterxml.jackson.databind.SerializationFeature;
19+
importcom.fasterxml.jackson.databind.SerializerProvider;
20+
importcom.fasterxml.jackson.databind.module.SimpleModule;
21+
importcom.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer;
22+
importcom.fasterxml.jackson.databind.util.Converter;
23+
importcom.fasterxml.jackson.databind.util.StdConverter;
24+
25+
// {{start:DeterministicObjectMapper}}
26+
publicclassDeterministicObjectMapper {
27+
28+
privateDeterministicObjectMapper() { }
29+
30+
publicstaticObjectMappercreate(ObjectMapperoriginal,CustomComparatorscustomComparators) {
31+
ObjectMappermapper =original.copy()
32+
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS,true)
33+
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY,true);
34+
35+
/*
36+
* Get the original instance of the SerializerProvider before we add our custom module.
37+
* Our Collection Delegating code does not call itself.
38+
*/
39+
SerializerProviderserializers =mapper.getSerializerProviderInstance();
40+
41+
// This module is reponsible for replacing non-deterministic objects
42+
// with deterministic ones. Example convert Set to a sorted List.
43+
SimpleModulemodule =newSimpleModule();
44+
module.addSerializer(Collection.class,
45+
newCustomDelegatingSerializerProvider(serializers,newCollectionToSortedListConverter(customComparators))
46+
);
47+
mapper.registerModule(module);
48+
returnmapper;
49+
}
50+
51+
/*
52+
* We need this class to delegate to the original SerializerProvider
53+
* before we added our module to it. If we have a Collection -> Collection converter
54+
* it delegates to itself and infinite loops until the stack overflows.
55+
*/
56+
privatestaticclassCustomDelegatingSerializerProviderextendsStdDelegatingSerializer
57+
{
58+
privatefinalSerializerProviderserializerProvider;
59+
60+
privateCustomDelegatingSerializerProvider(SerializerProviderserializerProvider,
61+
Converter<?, ?>converter)
62+
{
63+
super(converter);
64+
this.serializerProvider =serializerProvider;
65+
}
66+
67+
@Override
68+
protectedStdDelegatingSerializerwithDelegate(Converter<Object,?>converter,
69+
JavaTypedelegateType,JsonSerializer<?>delegateSerializer)
70+
{
71+
returnnewStdDelegatingSerializer(converter,delegateType,delegateSerializer);
72+
}
73+
74+
/*
75+
* If we do not override this method to delegate to the original
76+
* serializerProvider we get a stack overflow exception because it recursively
77+
* calls itself. Basically we are hijacking the Collection serializer to first
78+
* sort the list then delegate it back to the original serializer.
79+
*/
80+
@Override
81+
publicJsonSerializer<?>createContextual(SerializerProviderprovider,BeanPropertyproperty)
82+
throwsJsonMappingException
83+
{
84+
returnsuper.createContextual(serializerProvider,property);
85+
}
86+
}
87+
88+
privatestaticclassCollectionToSortedListConverterextendsStdConverter<Collection<?>,Collection<?>>
89+
{
90+
privatefinalCustomComparatorscustomComparators;
91+
92+
publicCollectionToSortedListConverter(CustomComparatorscustomComparators) {
93+
this.customComparators =customComparators;
94+
}
95+
@Override
96+
publicCollection<?extendsObject>convert(Collection<?>value)
97+
{
98+
if (value ==null ||value.isEmpty())
99+
{
100+
returnCollections.emptyList();
101+
}
102+
103+
/**
104+
* Sort all elements by class first, then by our custom comparator.
105+
* If the collection is heterogeneous or has anonymous classes its useful
106+
* to first sort by the class name then by the comparator. We don't care
107+
* about that actual sort order, just that it is deterministic.
108+
*/
109+
Comparator<Object>comparator =Comparator.comparing(x ->x.getClass().getName())
110+
.thenComparing(customComparators::compare);
111+
Collection<?extendsObject>filtered =Seq.seq(value)
112+
.filter(Objects::nonNull)
113+
.sorted(comparator)
114+
.toList();
115+
if (filtered.isEmpty())
116+
{
117+
returnCollections.emptyList();
118+
}
119+
120+
returnfiltered;
121+
}
122+
}
123+
124+
publicstaticclassCustomComparators {
125+
privatefinalLinkedHashMap<Class<?>,Comparator<?extendsObject>>customComparators;
126+
127+
publicCustomComparators() {
128+
customComparators =newLinkedHashMap<>();
129+
}
130+
131+
public <T>voidaddConverter(Class<T>clazz,Comparator<?>comparator) {
132+
customComparators.put(clazz,comparator);
133+
}
134+
135+
@SuppressWarnings({"unchecked","rawtypes" })
136+
publicintcompare(Objectfirst,Objectsecond) {
137+
// If the object is comparable use its comparator
138+
if (firstinstanceofComparable) {
139+
return ((Comparable)first).compareTo(second);
140+
}
141+
142+
// If the object is not comparable try a custom supplied comparator
143+
for (Entry<Class<?>,Comparator<?>>entry :customComparators.entrySet()) {
144+
Class<?>clazz =entry.getKey();
145+
if (first.getClass().isAssignableFrom(clazz)) {
146+
Comparator<Object>comparator = (Comparator<Object>)entry.getValue();
147+
returncomparator.compare(first,second);
148+
}
149+
}
150+
151+
// we have no way to order the collection so fail hard
152+
Stringmessage =String.format("Cannot compare object of type %s without a custom comparator",first.getClass().getName());
153+
thrownewUnsupportedOperationException(message);
154+
}
155+
}
156+
}
157+
// {{end:DeterministicObjectMapper}}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
packagecom.stubbornjava.common;
2+
3+
importstaticorg.junit.Assert.assertEquals;
4+
5+
importjava.util.Comparator;
6+
importjava.util.List;
7+
importjava.util.Map;
8+
importjava.util.Set;
9+
10+
importorg.jooq.lambda.Seq;
11+
importorg.junit.Before;
12+
importorg.junit.Test;
13+
14+
importcom.fasterxml.jackson.core.JsonProcessingException;
15+
importcom.fasterxml.jackson.databind.JsonMappingException;
16+
importcom.fasterxml.jackson.databind.ObjectMapper;
17+
importcom.google.common.collect.Lists;
18+
importcom.google.common.collect.Maps;
19+
importcom.google.common.collect.Sets;
20+
importcom.stubbornjava.common.DeterministicObjectMapper.CustomComparators;
21+
22+
// {{start:DeterministicObjectMapperTest}}
23+
publicclassDeterministicObjectMapperTest {
24+
25+
privateObjectMappermapper;
26+
27+
@Before
28+
publicvoidsetup() {
29+
CustomComparatorscustomComparators =newDeterministicObjectMapper.CustomComparators();
30+
mapper =DeterministicObjectMapper.create(Json.serializer().mapper(),customComparators);
31+
}
32+
33+
@Test
34+
publicvoidtestDeterministicSetInts()throwsJsonProcessingException
35+
{
36+
Set<Integer>ints =Sets.newLinkedHashSet(Lists.newArrayList(1,3,2));
37+
Stringactual =mapper.writer().writeValueAsString(ints);
38+
Stringexpected ="[1,2,3]";
39+
assertEquals(expected,actual);
40+
}
41+
42+
@Test
43+
publicvoidtestDeterministicSetStrings()throwsJsonProcessingException
44+
{
45+
Set<String>strings =Sets.newLinkedHashSet(Lists.newArrayList("a","c","b","aa","cc","bb"));
46+
Stringactual =mapper.writer().writeValueAsString(strings);
47+
Stringexpected ="[\"a\",\"aa\",\"b\",\"bb\",\"c\",\"cc\"]";
48+
assertEquals(expected,actual);
49+
}
50+
51+
@Test
52+
publicvoidtestHeterogeneousList()throwsJsonProcessingException
53+
{
54+
List<Object>strings =Lists.newArrayList("a",1,"b","c",2);
55+
Stringactual =mapper.writer().writeValueAsString(strings);
56+
Stringexpected ="[1,2,\"a\",\"b\",\"c\"]";
57+
assertEquals(expected,actual);
58+
}
59+
60+
@Test
61+
publicvoidtestDeterministicFieldOrder()throwsJsonProcessingException
62+
{
63+
@SuppressWarnings("unused")
64+
Objectdata =newObject() {
65+
publicStringget1() {return"1"; }
66+
publicStringgetC() {return"C"; }
67+
publicStringgetA() {return"A"; }
68+
};
69+
70+
Stringactual =mapper.writer().writeValueAsString(data);
71+
Stringexpected ="{\"1\":\"1\",\"a\":\"A\",\"c\":\"C\"}";
72+
assertEquals(expected,actual);
73+
}
74+
75+
@Test
76+
publicvoidtestDeterministicMapKeyOrder()throwsJsonProcessingException
77+
{
78+
Map<String,String>data =Maps.newLinkedHashMap();
79+
data.put("1","1");
80+
data.put("a","A");
81+
data.put("c","C");
82+
83+
Stringactual =mapper.writer().writeValueAsString(data);
84+
Stringexpected ="{\"1\":\"1\",\"a\":\"A\",\"c\":\"C\"}";
85+
assertEquals(expected,actual);
86+
}
87+
88+
@Test(expected=JsonMappingException.class)
89+
publicvoidtestCustomComparatorFails()throwsJsonProcessingException {
90+
Set<MyObject>objects =Seq.of(
91+
newMyObject(2),
92+
newMyObject(4),
93+
newMyObject(3),
94+
newMyObject(1)
95+
).toSet(Sets::newHashSet);
96+
97+
mapper.writer().writeValueAsString(objects);
98+
}
99+
100+
@Test
101+
publicvoidtestCustomComparatorPasses()throwsJsonProcessingException {
102+
CustomComparatorscomparators =newDeterministicObjectMapper.CustomComparators();
103+
comparators.addConverter(MyObject.class,Comparator.comparing(MyObject::getX));
104+
ObjectMappercustomizedComparatorsMapper =DeterministicObjectMapper.create(Json.serializer().mapper(),comparators);
105+
Set<MyObject>objects =Seq.of(
106+
newMyObject(2),
107+
newMyObject(4),
108+
newMyObject(3),
109+
newMyObject(1)
110+
).toSet();
111+
112+
Stringactual =customizedComparatorsMapper.writer().writeValueAsString(objects);
113+
Stringexpected ="[{\"x\":1},{\"x\":2},{\"x\":3},{\"x\":4}]";
114+
assertEquals(expected,actual);
115+
}
116+
117+
@Test
118+
publicvoidtestDeterministicNesting()throwsJsonProcessingException
119+
{
120+
@SuppressWarnings("unused")
121+
Objectobj =newObject() {
122+
publicStringget1() {return"1"; }
123+
publicStringgetC() {return"C"; }
124+
publicStringgetA() {return"A"; }
125+
};
126+
127+
Set<Integer>ints =Sets.newLinkedHashSet(Lists.newArrayList(1,4,2,3,5,7,8,9,6,0,50,100,99));
128+
129+
Map<String,Object>data =Maps.newLinkedHashMap();
130+
data.put("obj",obj);
131+
data.put("c","C");
132+
data.put("ints",ints);
133+
134+
Stringactual =mapper.writer().writeValueAsString(data);
135+
Stringexpected ="{" +
136+
"\"c\":\"C\"," +
137+
"\"ints\":[0,1,2,3,4,5,6,7,8,9,50,99,100]," +
138+
"\"obj\":{\"1\":\"1\",\"a\":\"A\",\"c\":\"C\"}" +
139+
"}";
140+
assertEquals(expected,actual);
141+
}
142+
143+
privatestaticclassMyObject {
144+
privatefinalintx;
145+
146+
publicMyObject(intx) {
147+
this.x =x;
148+
}
149+
150+
publicintgetX() {
151+
returnx;
152+
}
153+
}
154+
}
155+
// {{end:DeterministicObjectMapperTest}}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp