Skip to content

Commit 568e41f

Browse files
christophstroblmp911de
authored andcommitted
DATAREDIS-423 - Add support for Jackson2 based HashMapper.
Added FasterXML Jackson `ObjectMapper` based `HashMapper` implementation that allows flattening. ```java class Person { String firstname; String lastname; Address address; } class Address { String city; String country; } ``` ```bash firstname:Jon lastname:Snow address:{ city : Castle Black, country : The North } firstname:Jon lastname:Snow address.city:Castle Black address.country:The North ``` Original pull request: #197.
1 parent b1a3559 commit 568e41f

File tree

4 files changed

+599
-0
lines changed

4 files changed

+599
-0
lines changed

src/main/asciidoc/reference/redis.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ Multiple implementations are available out of the box:
365365

366366
1. `BeanUtilsHashMapper` using Spring's http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html[BeanUtils]
367367
2. `ObjectHashMapper` using <<redis.repositories.mapping>>
368+
3. `Jackson2HashMapper` using https://github.com/FasterXML/jackson[FasterXML Jackson].
368369

369370
[source,java]
370371
----
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/*
2+
* Copyright 2016 the original author or authors.
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+
package org.springframework.data.redis.hash;
17+
18+
import java.io.IOException;
19+
import java.util.ArrayList;
20+
import java.util.HashMap;
21+
import java.util.Iterator;
22+
import java.util.LinkedHashMap;
23+
import java.util.LinkedHashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Map.Entry;
27+
import java.util.Set;
28+
29+
import org.springframework.data.mapping.model.MappingException;
30+
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.StringUtils;
33+
34+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
35+
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
36+
import com.fasterxml.jackson.core.JsonParseException;
37+
import com.fasterxml.jackson.databind.DeserializationFeature;
38+
import com.fasterxml.jackson.databind.JsonMappingException;
39+
import com.fasterxml.jackson.databind.JsonNode;
40+
import com.fasterxml.jackson.databind.ObjectMapper;
41+
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
42+
import com.fasterxml.jackson.databind.SerializationFeature;
43+
44+
/**
45+
* {@link ObjectMapper} based {@link HashMapper} implementation that allows flattening. Given an entity {@code Person}
46+
* with an {@code Address} like below the flattening will create individual hash entries for all nested properties.
47+
*
48+
* <pre>
49+
* <code>
50+
* class Person {
51+
* String firstname;
52+
* String lastname;
53+
* Address address;
54+
* }
55+
*
56+
* class Address {
57+
* String city;
58+
* String country;
59+
* }
60+
* </code>
61+
* </pre>
62+
*
63+
* <pre>
64+
* <strong>Normal</strong><br />
65+
* <table>
66+
* <tr><td>firstname</td><td>Jon<td></tr>
67+
* <tr><td>lastname</td><td>Snow<td></tr>
68+
* <tr><td>address</td><td>{ city : Castle Black, country : The North }<td></tr>
69+
* </table>
70+
*
71+
* <strong>Flat</strong>: <br />
72+
* <table>
73+
*   <tr><td>firstname</td><td>Jon<td></tr>
74+
* <tr><td>lastname</td><td>Snow<td></tr>
75+
* <tr><td>address.city</td><td>Castle Black<td></tr>
76+
* <tr><td>address.country</td><td>The North<td></tr>
77+
* </table>
78+
* </pre>
79+
*
80+
* @author Christoph Strobl
81+
* @since 1.8
82+
*/
83+
public class Jackson2HashMapper implements HashMapper<Object, String, Object> {
84+
85+
private final ObjectMapper typingMapper;
86+
private final ObjectMapper untypedMapper;
87+
private final boolean flatten;
88+
89+
/**
90+
* Creates new {@link Jackson2HashMapper} with default {@link ObjectMapper}.
91+
*
92+
* @param flatten
93+
*/
94+
public Jackson2HashMapper(boolean flatten) {
95+
96+
this(new ObjectMapper(), flatten);
97+
98+
typingMapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
99+
typingMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
100+
typingMapper.setSerializationInclusion(Include.NON_NULL);
101+
typingMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
102+
}
103+
104+
/**
105+
* Creates new {@link Jackson2HashMapper}.
106+
*
107+
* @param mapper must not be {@literal null}.
108+
* @param flatten
109+
*/
110+
public Jackson2HashMapper(ObjectMapper mapper, boolean flatten) {
111+
112+
Assert.notNull(mapper, "Mapper must not be null!");
113+
114+
this.typingMapper = mapper;
115+
this.flatten = flatten;
116+
117+
this.untypedMapper = new ObjectMapper();
118+
this.untypedMapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
119+
this.untypedMapper.setSerializationInclusion(Include.NON_NULL);
120+
}
121+
122+
/*
123+
* (non-Javadoc)
124+
* @see org.springframework.data.redis.hash.HashMapper#toHash(java.lang.Object)
125+
*/
126+
@Override
127+
@SuppressWarnings("unchecked")
128+
public Map<String, Object> toHash(Object source) {
129+
130+
JsonNode tree = typingMapper.valueToTree(source);
131+
return flatten ? flattenMap(tree.fields()) : untypedMapper.convertValue(tree, Map.class);
132+
}
133+
134+
/*
135+
* (non-Javadoc)
136+
* @see org.springframework.data.redis.hash.HashMapper#fromHash(java.util.Map)
137+
*/
138+
@Override
139+
public Object fromHash(Map<String, Object> hash) {
140+
141+
try {
142+
143+
if (flatten) {
144+
145+
return typingMapper.reader().forType(Object.class)
146+
.readValue(untypedMapper.writeValueAsBytes(doUnflatten(hash)));
147+
}
148+
149+
return typingMapper.treeToValue(untypedMapper.valueToTree(hash), Object.class);
150+
151+
} catch (JsonParseException e) {
152+
throw new MappingException(e.getMessage(), e);
153+
} catch (JsonMappingException e) {
154+
throw new MappingException(e.getMessage(), e);
155+
} catch (IOException e) {
156+
throw new MappingException(e.getMessage(), e);
157+
}
158+
}
159+
160+
@SuppressWarnings("unchecked")
161+
private Map<String, Object> doUnflatten(Map<String, Object> source) {
162+
163+
Map<String, Object> result = new LinkedHashMap<String, Object>();
164+
Set<String> treatSeperate = new LinkedHashSet<String>();
165+
for (Entry<String, Object> entry : source.entrySet()) {
166+
167+
String key = entry.getKey();
168+
String[] args = key.split("\\.");
169+
170+
if (args.length == 1 && !args[0].contains("[")) {
171+
result.put(entry.getKey(), entry.getValue());
172+
continue;
173+
}
174+
175+
if (args.length == 1 && args[0].contains("[")) {
176+
177+
String prunedKey = args[0].substring(0, args[0].indexOf('['));
178+
if (result.containsKey(prunedKey)) {
179+
appendValueToTypedList(args[0], entry.getValue(), (List<Object>) result.get(prunedKey));
180+
} else {
181+
result.put(prunedKey, createTypedListWithValue(entry.getValue()));
182+
}
183+
} else {
184+
treatSeperate.add(key.substring(0, key.indexOf('.')));
185+
}
186+
}
187+
188+
for (String partial : treatSeperate) {
189+
190+
Map<String, Object> newSource = new LinkedHashMap<String, Object>();
191+
192+
for (Entry<String, Object> entry : source.entrySet()) {
193+
if (entry.getKey().startsWith(partial)) {
194+
newSource.put(entry.getKey().substring(partial.length() + 1), entry.getValue());
195+
}
196+
}
197+
198+
if (partial.endsWith("]")) {
199+
200+
String prunedKey = partial.substring(0, partial.indexOf('['));
201+
202+
if (result.containsKey(prunedKey)) {
203+
appendValueToTypedList(partial, doUnflatten(newSource), (List<Object>) result.get(prunedKey));
204+
} else {
205+
result.put(prunedKey, createTypedListWithValue(doUnflatten(newSource)));
206+
}
207+
} else {
208+
result.put(partial, doUnflatten(newSource));
209+
}
210+
}
211+
212+
return result;
213+
}
214+
215+
private Map<String, Object> flattenMap(Iterator<Entry<String, JsonNode>> source) {
216+
217+
Map<String, Object> resultMap = new HashMap<String, Object>();
218+
this.doFlatten("", source, resultMap);
219+
return resultMap;
220+
}
221+
222+
private void doFlatten(String propertyPrefix, Iterator<Entry<String, JsonNode>> inputMap,
223+
Map<String, Object> resultMap) {
224+
225+
if (StringUtils.hasText(propertyPrefix)) {
226+
propertyPrefix = propertyPrefix + ".";
227+
}
228+
229+
while (inputMap.hasNext()) {
230+
231+
Entry<String, JsonNode> entry = inputMap.next();
232+
flattenElement(propertyPrefix + entry.getKey(), entry.getValue(), resultMap);
233+
}
234+
}
235+
236+
private void flattenElement(String propertyPrefix, Object source, Map<String, Object> resultMap) {
237+
238+
if (!(source instanceof JsonNode)) {
239+
240+
resultMap.put(propertyPrefix, source);
241+
return;
242+
}
243+
244+
JsonNode element = (JsonNode) source;
245+
if (element.isArray()) {
246+
247+
Iterator<JsonNode> nodes = element.elements();
248+
249+
while (nodes.hasNext()) {
250+
251+
JsonNode cur = nodes.next();
252+
if (cur.isArray()) {
253+
this.falttenCollection(propertyPrefix, cur.elements(), resultMap);
254+
}
255+
}
256+
257+
} else if (element.isContainerNode()) {
258+
this.doFlatten(propertyPrefix, element.fields(), resultMap);
259+
} else {
260+
resultMap.put(propertyPrefix, new DirectFieldAccessFallbackBeanWrapper(element).getPropertyValue("_value"));
261+
}
262+
}
263+
264+
private void falttenCollection(String propertyPrefix, Iterator<JsonNode> list, Map<String, Object> resultMap) {
265+
266+
int counter = 0;
267+
while (list.hasNext()) {
268+
JsonNode element = list.next();
269+
flattenElement(propertyPrefix + "[" + counter + "]", element, resultMap);
270+
counter++;
271+
}
272+
}
273+
274+
@SuppressWarnings("unchecked")
275+
private void appendValueToTypedList(String key, Object value, List<Object> destination) {
276+
277+
int index = Integer.valueOf(key.substring(key.indexOf('[') + 1, key.length() - 1)).intValue();
278+
List<Object> resultList = ((List<Object>) destination.get(1));
279+
if (resultList.size() < index) {
280+
resultList.add(value);
281+
} else {
282+
resultList.add(index, value);
283+
}
284+
}
285+
286+
private List<Object> createTypedListWithValue(Object value) {
287+
288+
List<Object> listWithTypeHint = new ArrayList<Object>();
289+
listWithTypeHint.add(ArrayList.class.getName()); // why jackson? why?
290+
List<Object> values = new ArrayList<Object>();
291+
values.add(value);
292+
listWithTypeHint.add(values);
293+
return listWithTypeHint;
294+
}
295+
}

0 commit comments

Comments
 (0)