Could not deserialize multimap with XStream / Jettison - java

I'm trying to serialize and deserialize a guava's multimap using XStream and Jettison. Here's a simple test to illustrate:
final XStream xstream = new XStream(new JettisonMappedXmlDriver());
final Multimap<TestEnum, String> test = HashMultimap.create();
test.put(TestEnum.E1, "test");
final String json = xstream.toXML(test);
final Multimap<TestEnum, String> result = (Multimap<TestEnum, String>)xstream.fromXML(json);
It gives the following error:
com.thoughtworks.xstream.converters.ConversionException: Could not call com.google.common.collect.HashMultimap.readObject() : com.test.Test$TestEnum cannot be cast to java.lang.Integer
---- Debugging information ----
message : Could not call com.google.common.collect.HashMultimap.readObject()
cause-exception : java.lang.ClassCastException
cause-message : com.test.Test$TestEnum cannot be cast to java.lang.Integer
class : com.google.common.collect.HashMultimap
required-type : com.google.common.collect.HashMultimap
converter-type : com.thoughtworks.xstream.converters.reflection.SerializableConverter
path : /com.google.common.collect.HashMultimap/com.google.common.collect.HashMultimap
line number : -1
version : 1.4.7
-------------------------------
Note that this error especially focus on Multimap when used with an enum key. If I use Map instead of multimap, there's no error. If I use String instead of Enum as key, there's no error. Also, if I serialize to XML instead of JSON (that is, without "JettisonMappedXmlDriver" in constructor), it works perfectly.
Is there a good solution for this? I'm currently using a workaround, replacing my multimap with a map of collection, but I would prefer to find a way to keep multimap.

The solution serialized a Multimap with XStream is to use Multimap converter and register to XStream, like this:
public class MultimapConverter extends AbstractCollectionConverter {
public MultimapConverter(Mapper mapper) {
super(mapper);
}
#Override
public boolean canConvert(#SuppressWarnings("rawtypes") Class clazz) {
return Multimap.class.isAssignableFrom(clazz);
}
#Override
#SuppressWarnings("unchecked")
public void marshal(
Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
Multimap map = (Multimap) value;
for (Object key : map.keySet()) {
for (Object v : map.get(key)) {
if (v != null) {
writer.startNode("entry");
writer.addAttribute("key", key.toString());
writeCompleteItem(v, context, writer);
writer.endNode();
}
}
}
}
#Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
ArrayListMultimap<Object, Object> map = ArrayListMultimap.create();
while (reader.hasMoreChildren()) {
reader.moveDown();
String key = reader.getAttribute("key");
Object value = null;
while (reader.hasMoreChildren()) {
reader.moveDown();
value = readBareItem(reader, context, map);
reader.moveUp();
}
reader.moveUp();
if (value != null) {
map.put(key, value);
}
}
return map;
}
}
Then register it to the XStream serializer you have, for example:
XStream xstream = new XStream(new JettisonMappedXmlDriver());
xstream.registerConverter(new MultimapConverter(xstream.getMapper()));
xstream.allowTypeHierarchy(Multimap.class);
xstream.addDefaultImplementation(ArrayListMultimap.class, Multimap.class);
As for the error on deserializing the above converter works as expected:
#Test
public void testSerializeWithEnum() {
XStream xstream = new XStream(new JettisonMappedXmlDriver());
xstream.registerConverter(new MultimapConverter(xstream.getMapper()));
xstream.allowTypeHierarchy(Multimap.class);
xstream.addDefaultImplementation(ArrayListMultimap.class, Multimap.class);
Multimap<TestEnum, String> test = HashMultimap.create();
test.put(TestEnum.E1, "test");
String json = xstream.toXML(test);
final Multimap<TestEnum, String> result = (Multimap<TestEnum, String>)xstream.fromXML(json);
}
public enum TestEnum {
E1
}
If you put a breakpoint and debug the result variable here you would see that the JSON was deserialized with the value of the enum.

Related

How can I transform a Map with a value of Object to the appropriate type?

I am working on a project where I need to accept a Map called properties that is of type Map<String, Object>. There are going to be potentially many different keys in this Map, but I only care about one: xpath. An xpath can have one of three different types of values:
A string, such as {"xpath": "path/to/xml/tag"}
A List of xpaths, such as: {"xpath": ["path/to/xml/tag1", "tag2", "path/tag3"}
A Map<String, Map<String, Boolean>>, such as:
{
"xpath":
{
"path/to/xml":
{
"setting1?": true,
"setting2?": true
},
"path/tag2":
{
"setting1?": false,
"setting2": true
},
"path/to/tag3": null
}
}
Now I have three variables: String xpath, Set<String> xpaths, Map<String, Map<String, boolean> xpathMap. I have a function that is supposed to try and map the values of the "xpath" key in the properties map, and it looks like this:
private void decideXPathType(Map<String, Object> properties)
{
Object propertiesXPath = properties.get("xpath");
if (propertiesXPath instanceof String)
{
this.xpath = (String) propertiesXPath;
} else if (propertiesXPath instanceof List)
{
this.xpaths = new HashSet<String>((List) propertiesXPath);
} else if (propertiesXPath instanceof Map)
{
for (Object key : ((Map) propertiesXPath).keySet())
{
Map<String, Boolean> value = (Map<String, Boolean>) ((Map) propertiesXPath).get(key);
this.xpathMap.put((String) key, value);
}
} else
{
throw new IllegalArgumentException("the xpath value is neither String, List, or Map<String, Boolean>");
}
}
But this function looks so bad - there is so much casting, etc - and although it works, it just looks too messy, and I imagine something can go wrong... any ideas on how I can make this cleaner?
Edit: Some more details
The properties map is originally a json JsonNode requestBody that I receive from a service. Using ObjectMapper, I create a properties map as such:
Map<String, Object> properties = new ObjectMapper().convertValue(new ObjectMapper().readTree(requestBody), new TypeReference<Map<String, Object>>(){});
If I receive a json string that is the value of the xpathMap example that I gave, I get something that looks like this:
Hope this information helps?
In your JSON, use different keys for these different types of values: String, List and Map. Deserializing a map:
#Test
public void test0() throws IOException {
ObjectMapper om = new ObjectMapper();
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("xpath-object.json");
JsonNode jsonNode = om.readTree(inputStream);
Map<String, Map<String, Boolean>> value = om.readValue(jsonNode.get("xpath").toString(), Map.class);
// prints {path/to/xml={setting1?=true, setting2?=true}, path/to/tag3=null, path/tag2={setting1?=false, setting2=true}}
System.out.println(value);
}
If you need to work with 3rd party JSON, you can use following approach:
#Test
public void test() throws IOException {
testThemAll("xpath-scalar.json");
testThemAll("xpath-array.json");
testThemAll("xpath-object.json");
// prints:
// path/to/xml/tag
// [path/to/xml/tag1, tag2, path/tag3]
// {path/to/xml={setting1?=true, setting2?=true}, path/to/tag3=null, path/tag2={setting1?=false, setting2=true}}
}
private void testThemAll(String fileName) throws IOException {
ObjectMapper om = new ObjectMapper();
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(fileName);
JsonNode jsonNode = om.readTree(inputStream).get("xpath");
if (jsonNode.isValueNode())
System.out.println(jsonNode.asText());
else if (jsonNode.isArray()) {
System.out.println(om.readValue(jsonNode.toString(), List.class));
} else if (jsonNode.isObject()) {
Map<String, Map<String, Boolean>> value = om.readValue(jsonNode.toString(), Map.class);
System.out.println(value);
}
}

XStream: problems with Generic LinkedHashMap

I am using XStream 1.4.8 and trying to serialize a generic LinkedHashMap<?,?>.
LinkedHashMap does not appear to maintain its order when serialized by XStream. I need to write a new Converter for it so that it does.
The problem is exacerbated by the fact that I have several different types of the Generic class LinkedHashMap in use, and I would like to need only one Converter that works for the generic version.
In other words:
given an a arbitrary Object to serialize that may contain several different kinds of fields of type LinkedHashMap<?,?>, how do you marshal and unmarshal them all, with the correct Types used for each of the generics, and with the order maintained in each of the LinkedHashMaps?
This question is similar, but it's not for Generics, and is also based on an older version of XStream:
Xstream does not maintain order of child elements while unmarshalling
You can get some ideas in this source code:
https://github.com/bluesoft-rnd/aperte-workflow-core/blob/master/core/activiti-context/src/main/java/org/aperteworkflow/ext/activiti/ActivitiStepAction.java#L47
Map params = new HashMap();
if (this.params != null) {
String xml = (String) this.params.getValue(execution);
if (xml != null) {
XStream xs = new XStream();
xs.alias("map", java.util.Map.class);
xs.registerConverter(new Converter() {
public boolean canConvert(Class clazz) {
return AbstractMap.class.isAssignableFrom(clazz);
}
public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
AbstractMap<String, String> map = (AbstractMap<String, String>) value;
for (Map.Entry<String, String> entry : map.entrySet()) {
writer.startNode(entry.getKey().toString());
writer.setValue(entry.getValue().toString());
writer.endNode();
}
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
Map<String, String> map = new HashMap<String, String>();
while (reader.hasMoreChildren()) {
reader.moveDown();
map.put(reader.getNodeName(), reader.getValue());
reader.moveUp();
}
return map;
}
});
params = (Map) xs.fromXML(xml);
}
}
Some examples here:
/**
* Checks whether the XStream-based REST converters should be used for
* rendering the response of this REST request.
* <p>
* This method checks whether the request has a query attribute named
* <tt>"rest"</tt>, or whether there is a system-wide
* {#link BundleProperties#getProperty(String) bundle/system property}
* named <tt>"skalli.rest"</tt> with a value <i>different</i> from <tt>"v1"</tt>.
* In that case, e.g. the request has a query attribute <tt>"rest=v2"</tt>,
* the method returns <code>false</code> to indicate that the new
* RestWriter-based converters should be employed. Otherwise the
* method returns <code>true</code>.
* <p>
* If the requested media type is different from <tt>"text/xml"</tt>,
* always <code>false</code> will be returned.
*
* #return <code>true</code>, if XStream-based converters should be used
* for rendering the response of this REST request.
*/
#SuppressWarnings("nls")
protected boolean enforceOldStyleConverters() {
if (!context.isXML()) {
return false;
}
String restVersion = getQueryAttribute("rest");
if (StringUtils.isBlank(restVersion)) {
restVersion = BundleProperties.getProperty("skalli.rest");
}
if (StringUtils.isNotBlank(restVersion)) {
return "v1".equalsIgnoreCase(restVersion);
}
return true;
}
Full source code here:
http://code.openhub.net/file?fid=FMrVl1G9kYhg416Lk5dachOp98c&cid=br-mQGOdySQ&s=XStream%3A%20problems%20with%20Generic%20LinkedHashMap&pp=0&fl=Java&ff=1&filterChecked=true&fp=390342&mp,=1&ml=1&me=1&md=1&projSelected=true#L0
After some trial and error, I found the solution. For anyone interested, here it is:
It turns out that classes being generic are not actually as much of a problem as I feared. Ironically, Java's compile-time interpretation, which is usually annoying,ends up being to our benefit here: It actually suffices to create a LinkedHashMap with only Object as the generic types.
However, one does need to take care to ensure that the objects stored in the map are unmarshalled as the correct class. This can be done easily enough by just storing the class information for each entry of the map. Note that it does not suffice to store the class information only once for the entire map, since some entries of the map may be subclasses of that class.
private static class LinkedHashMapConverter implements Converter {
#SuppressWarnings("rawtypes")
#Override
public boolean canConvert(Class clazz) {
return clazz.equals(LinkedHashMap.class);
}
#Override
public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
#SuppressWarnings("unchecked")
LinkedHashMap<Object, Object> map = (LinkedHashMap<Object, Object>) value;
// store each entry
for (Entry<Object, Object> a : map.entrySet()) {
writer.startNode("entry");
// store the key, the value, and the types of both
writer.startNode("keyClass");
context.convertAnother(a.getKey().getClass());
writer.endNode();
writer.startNode("key");
context.convertAnother(a.getKey());
writer.endNode();
writer.startNode("valueClass");
context.convertAnother(a.getValue().getClass());
writer.endNode();
writer.startNode("value");
context.convertAnother(a.getValue());
writer.endNode();
writer.endNode();
}
}
#SuppressWarnings("rawtypes")
#Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
LinkedHashMap<Object, Object> res = new LinkedHashMap<Object, Object>();
while (reader.hasMoreChildren()) {
reader.moveDown();
// load the key, the value, and the types of both
reader.moveDown();
Class keyClass = (Class) context.convertAnother(res, Class.class);
reader.moveUp();
reader.moveDown();
Object key = context.convertAnother(res, keyClass);
reader.moveUp();
reader.moveDown();
Class valueClass = (Class) context.convertAnother(res, Class.class);
reader.moveUp();
reader.moveDown();
Object value = context.convertAnother(res, valueClass);
reader.moveUp();
res.put(key, value);
reader.moveUp();
}
return res;
}
}

How to convert Java class to Map<String, String> and convert non-string members to json using jackson?

I have some class in Java that I want to convert to a Map<String, String>. The catch is that any fields of my java class that don't have an obvious String representation (collections, other classes) should be converted to json strings.
Here's an example:
#Data
#AllArgsConstructor
class MyClass {
String field1;
Long field2;
Set<String> field3;
OtherClass field4;
}
#Data
#AllArgsConstructor
class OtherClass {
String field1;
String field2;
}
ObjectMapper mapper = new ObjectMapper();
MyClass myClass = new MyClass("value",
123L,
Sets.newHashSet("item1", "item2"),
new OtherClass("value1", "value2"));
Map<String, String> converted =
mapper.convertValue(myClass, new TypeReference<Map<String, String>>(){});
At this point, converted should look like the following:
"field1" -> "value"
"field2" -> "123"
"field3" -> "[\"item1\", \"item2\"]"
"field4" -> "{\"field1\":\"value1\",\"field2\":\"value2\"}"
Instead, the call to mapper.convertValue fails when trying to deserizlize the Set with the exception java.lang.IllegalArgumentException: Can not deserialize instance of java.lang.String out of START_ARRAY token.
Are there any special configurations I can annotate MyClass with or ways to configure the ObjectMapper to make this work the way I want it to?
Here's one way to do it.
private static final ObjectMapper mapper = new ObjectMapper();
public Map<String, String> toMap(Object obj) {
// Convert the object to an intermediate form (map of strings to JSON nodes)
Map<String, JsonNode> intermediateMap = mapper.convertValue(obj, new TypeReference<Map<String, JsonNode>>() {});
// Convert the json nodes to strings
Map<String, String> finalMap = new HashMap<>(intermediateMap.size() + 1); // Start out big enough to prevent resizing
for (Map.Entry<String, JsonNode> e : intermediateMap.entrySet()) {
String key = e.getKey();
JsonNode val = e.getValue();
// Get the text value of textual nodes, and convert non-textual nodes to JSON strings
String stringVal = val.isTextual() ? val.textValue() : val.toString();
finalMap.put(key, stringVal);
}
return finalMap;
}
And if you want to convert the Map<String, String> back to the original class...
public static <T> T fromMap(Map<String, String> map, Class<T> clazz) throws IOException {
// Convert the data to a map of strings to JSON nodes
Map<String, JsonNode> intermediateMap = new HashMap<>(map.size() + 1); // Start out big enough to prevent resizing
for (Map.Entry<String, String> e : map.entrySet()) {
String key = e.getKey();
String val = e.getValue();
// Convert the value to the right type of JsonNode
JsonNode jsonVal;
if (val.startsWith("{") || val.startsWith("[") || "null".equals(val)) {
jsonVal = mapper.readValue(val, JsonNode.class);
} else {
jsonVal = mapper.convertValue(val, JsonNode.class);
}
intermediateMap.put(key, jsonVal);
}
// Convert the intermediate map to an object
T result = mapper.convertValue(intermediateMap, clazz);
return result;
}

Converting XML to Java Map<String, Integer> using XStream

I'm converting XML code to a Java Map. The XML matches a large number of random words with a number (a probability distribution) and looks like this:
<?xml version="1.0" encoding="UTF-8" ?>
<root>
<Durapipe type="int">1</Durapipe>
<EXPLAIN type="int">2</EXPLAIN>
<woods type="int">2</woods>
<hanging type="int">3</hanging>
<hastily type="int">2</hastily>
<localized type="int">1</localized>
.......
</root>
I'm trying to implement this with XStream. Here's the Java code that my main program currently uses:
XStream xstream = new XStream();
Map<String, Integer> englishCorpusProbDist;
xstream.registerConverter(new MapEntryConverter());
englishCorpusProbDist = (Map<String, Integer>)xstream.fromXML(new File("C:/Users/David Naber/Documents/IREP Project/frequencies.xml"));
And here's my MapEntryConverterClass:
public class MapEntryConverter implements Converter {
public boolean canConvert(Class clazz) {
return Map.class.isAssignableFrom(clazz);
}
public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
Map<String, Integer> map = (Map<String, Integer>) value;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
writer.startNode(entry.getKey().toString());
writer.setValue(entry.getValue().toString());
writer.endNode();
}
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
Map<String, Integer> map = new HashMap<String, Integer>();
while (reader.hasMoreChildren()) {
reader.moveDown();
map.put(reader.getNodeName(), reader.getValue());
reader.moveUp();
}
return map;
}
}
I"m getting an error in the above function, on the line "map.put(reader.getNodeName(), reader.getValue());". The error says: "The method put(String, Integer) in the type Map is not applicable for the arguments (String, String)."
So I really have two questions here. First of all, why is this error happening and how can I fix it? Secondly, what more will I need to implement to finally get XStream to convert this to XML?
Any help is much appreciated. Thank you in advance!
Yes error is correct reader.getValue() is giving String , You must have to Type Cast it in Integer
Change below code
map.put(reader.getNodeName(), reader.getValue());
to
map.put(reader.getNodeName(), new Integer(reader.getValue()));
This is my example for more complex data with nested Maps
public static class MapEntryConverter implements Converter {
static final Converter INSTANCE = new MapEntryConverter();
public boolean canConvert(Class clazz) {
return Map.class.isAssignableFrom(clazz);
}
public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
Map map = (Map) value;
for (Object obj : map.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
writer.startNode(entry.getKey().toString());
Object val = entry.getValue();
if (val != null) context.convertAnother(val);
writer.endNode();
}
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
while (reader.hasMoreChildren()) {
reader.moveDown();
String key = reader.getNodeName();
Object value = null;
if (reader.hasMoreChildren()) {
value = unmarshal(reader, context);
} else {
value = reader.getValue();
}
map.put(key, value);
reader.moveUp();
}
return map;
}
}
Have fun!

Calling method with generics

I'm working with a service which returns JSON which can be converted to Map (I'm using google-gson lib for converting). I need to get Set of values from this Map.
First, I had the next structure:
public Set<ProfileShow> getShows() {
String json = ...; //getting JSON from service
if (!Utils.isEmptyString(json)) {
Map<String, ProfileShow> map = Utils.fromJSON(json, new TypeToken<Map<String, ProfileShow>>() {
}.getType());
Set<ProfileShow> result = new HashSet<ProfileShow>();
for (String key : map.keySet()) {
result.add(map.get(key));
}
return result;
}
return Collections.emptySet();
}
public Set<Episode> getUnwatchedEpisodes() {
String json = ...; //getting JSON from service
if (!Utils.isEmptyString(json)) {
Map<String, Episode> map = Utils.fromJSON(json, new TypeToken<Map<String, Episode>>() {
}.getType());
Set<Episode> result = new HashSet<Episode>();
for (String key : map.keySet()) {
result.add(map.get(key));
}
return result;
}
return Collections.emptySet();
}
Utils.fromJSON method:
public static <T> T fromJSON(String json, Type type) {
return new Gson().fromJson(json, type);
}
As you can see, methods getShows() and getUnwatchedEpisodes() has the same structure. Only difference is a parametrized type of the returning Set. So, I decided to move getting Set from JSON to util method:
public static <T> Set<T> setFromJSON(String json, T type) {
if (!isEmptyString(json)) {
Map<String, T> map = fromJSON(json, new TypeToken<Map<String, T>>() {
}.getType());
Set<T> result = new HashSet<T>();
for (String key : map.keySet()) {
result.add(map.get(key));
}
return result;
}
return Collections.emptySet();
}
But now I'm stuck how to call this method in a proper way. Something like
Utils.setFromJSON(json, Episode.class.getGenericSuperclass()); //doesn't work
Thanks for your help.
Perhaps the easiest thing to do is change the type of type to Type, and pass in new TypeToken<Map<String, ProfileShow>>() { }.getType() or similar.
I guess you could construct the ParameterizedType if you really wanted.

Categories