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!
Related
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);
}
}
I've recently been faced with a tough json (that I don't control, so I have to deal with it):
{
"someOtherParam":"someValue",
"attributes":
{
"language":["fr", "en"],
"otherParam":["value1", "value2"]
}
}
attributes is a map - I don't know what attributes it may contain, so I can't just map it to an object. In Java I believe I need to map is as a Map<String,List<String>> somehow.
I've found a very helpful post that allowed me to write an adapter like this:
public class MapAdapter extends XmlAdapter<MapAdapter.AdaptedMap, Map<String, List<String>>> {
public static class AdaptedMap {
#XmlVariableNode("key")
List<AdaptedEntry> entries = new ArrayList<>();
}
public static class AdaptedEntry {
#XmlTransient
public String key;
#XmlValue
public List<String> value;
}
#Override
public AdaptedMap marshal(Map<String, List<String>> map) throws Exception {
AdaptedMap adaptedMap = new AdaptedMap();
for(Map.Entry<String, List<String>> entry : map.entrySet()) {
AdaptedEntry adaptedEntry = new AdaptedEntry();
adaptedEntry.key = entry.getKey();
adaptedEntry.value = entry.getValue();
adaptedMap.entries.add(adaptedEntry);
}
return adaptedMap;
}
#Override
public Map<String, List<String>> unmarshal(AdaptedMap adaptedMap) throws Exception {
List<AdaptedEntry> adaptedEntries = adaptedMap.entries;
Map<String, List<String>> map = new HashMap<>(adaptedEntries.size());
for(AdaptedEntry adaptedEntry : adaptedEntries) {
map.put(adaptedEntry.key, adaptedEntry.value);
}
return map;
}
}
And while this general approach would work for simple values (so a Map<String,String> for instance), here on marshalling it insists on mapping the list as a simple element
{
"someOtherParam":"someValue",
"attributes":
{
"language":"fr en",
"otherParam":"value1 value2"
}
}
So how do I do this correctly?
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;
}
}
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.
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.