I have through various reasons ended up in a situation where I need to deserialize a json object from Fable F# in android studio with java.
The string is as follows:
{"MainForm":{"OriginMerd":{"id":"onMggagerd","number":5,"tests":{"Blod":{"blodid":"1","glucose":52}}}}}
the code:
Stream<Map.Entry<String, String>> flatten(Map<String, Object> map)
{
return map.entrySet()
.stream()
.flatMap(this::extractValue);
}
Stream<Map.Entry<String, String>> extractValue(Map.Entry<String, Object> entry) {
if (entry.getValue() instanceof String) {
return Stream.of(new AbstractMap.SimpleEntry(entry.getKey(), (String) entry.getValue()));
} else if (entry.getValue() instanceof Map) {
return flatten((Map<String, Object>) entry.getValue());
}
return null;
}
#ReactMethod
public void testFunc(String jsonString, Callback cb){
Map<String,Object> map = new HashMap<>();
ObjectMapper mapper = new ObjectMapper();
try {
//convert JSON string to Map
map = mapper.readValue(String.valueOf(jsonString), new TypeReference<Map<String, Object>>() {
});
Map<String, String> flattenedMap = flatten(map)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
for (Map.Entry<String, String> entry : flattenedMap.entrySet()) {
Log.e("flatmap",entry.getKey() + "/" + entry.getValue());
//System.out.println(entry.getKey() + "/" + entry.getValue());
}
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Log.e("JSONSTRING", jsonString);
cb.invoke("OK");
}
First I figured I'd make it into a map with object mapper as such I used the object mapper to get a map of then I followed this approach
How to Flatten a HashMap?
However the issue with this is that the result only gives me the orginMerd id and the blodid, not the number or glucose fields. Is there a elegant way to achieve this? I am unfortunately not very well versed in Java.
Paste the json you need to deserialize here. Select source-type JSON, deselect 'allow additional properties', input your package name and your class name. It's gonna generate Java classes (source code, not the compiled .class files) for you that fit your json.
Download generated sources, put them into your project and then just do: objectMapper.readValue(string, YourClassName.class);. YourClassName is the class name you input into the site (not MainForm class, be careful, I fell into that trap while testing this just now).
In your current version you are only allowing String values. You should change that to allow other simple types. To determine that you can use this method:
private static boolean isSimpleType(Class<?> type) {
return type.isPrimitive() ||
Boolean.class == type ||
Character.class == type ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Enum.class.isAssignableFrom(type);
}
Or look here for more details on how to determine if a class is simple. You also can simply adjust that methods to fit your needs.
With this you can use the following to flatten your map:
public static Stream<Map.Entry<String, Object>> flatten(Map<String, Object> map) {
return map.entrySet()
.stream()
.flatMap(YourClass::extractValue);
}
private static Stream<Map.Entry<String, Object>> extractValue(Map.Entry<String, Object> entry) {
if (isSimpleType(entry.getValue().getClass())) {
return Stream.of(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue()));
} else if (entry.getValue() instanceof Map) {
return flatten((Map<String, Object>) entry.getValue());
}
return null;
}
And call it like this as before:
Map<String, Object> flattenedMap = flatten(map)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
flattenedMap.forEach((key, value) -> System.out.println(key + ": " + value));
You handle only the cases for values of type string or map of your json :
if (entry.getValue() instanceof String) {
return Stream.of(new AbstractMap.SimpleEntry(entry.getKey(), (String) entry.getValue()));
} else if (entry.getValue() instanceof Map) {
return flatten((Map<String, Object>) entry.getValue());
}
To get number or glucose entries, you should also handle the Number type with Long or Integer according to what you get from the JSON deserialization.
The problem with this approach is that Integer and Long are not String and actually you map the json entries to Stream<Map.Entry<String, String>>.
Putting numbers in String variables is possible with toString() :
For example to handle both number and string :
final String value = entry.getValue();
if (value instanceof String || value instanceof Number) {
return Stream.of(new AbstractMap.SimpleEntry(entry.getKey(),
value.toString()));
}
else if (value instanceof Map) {
return flatten((Map<String, Object>) value);
}
// you may return null but you will at least log something
else {
LOGGER.warn("field with key {} and value {} not mapped", entry.getKey(), value);
return null;
}
But it will also make the content of your Map unclear requiring some checks before using it.
Similarly using a generic type like Stream<Map.Entry<String, Object>> will create a similar issue.
So I think that you could consider using a specific class to represent your model and use it during deserialization.
Foo foo = mapper.readValue(String.valueOf(jsonString), Foo.class);
where Foo is a class describing the expected structure.
Related
Basically I have a List<Map<String, Object>> listOfValueand I need to check if the object is instance of byte then encode it to String as shown below:
private void convertByteToBase64(List<Map<String, Object>> listOfValue) {
Object value = null;
if (!CollectionUtils.isEmpty(listOfValue)) {
for (Map<String, Object> map : listOfValue) {
if (!map.isEmpty()) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
value = entry.getValue();
if (value instanceof byte[]) {
entry.setValue(Base64.getEncoder().encodeToString((byte[]) value));
}
}
}
}
}
}
I am using java 8 and it is working as expected but is it the correct way of doing it or any better way in term of performance?
Current implementation seems to be ok, however, checking for emptiness of the list and the nested maps seems to be redundant.
Some performance improvement may be possibly achieved if parallel streams are used to iterate the list/maps.
private void convertByteToBase64(List<Map<String, Object>> listOfValue) {
Base64.Encoder base64encoder = Base64.getEncoder();
listOfValue.parallelStream()
.flatMap(map -> map.entrySet().parallelStream())
.filter(entry -> entry.getValue() instanceof byte[])
.forEach(entry -> entry.setValue(
base64encoder.encodeToString((byte[]) entry.getValue())
));
}
Base64.Encoder is thread-safe: Instances of Base64.Encoder class are safe for use by multiple concurrent threads..
Alternatively, you can use parallelStream and filter as below.
private void convertByteToBase64(List<Map<String, Object>> listOfValue) {
listOfValue.parallelStream().forEach(map -> {
map.entrySet().parallelStream().filter(entry->entry.getValue() instanceof byte[]).forEach(entry -> {
entry.setValue(Base64.getEncoder().encodeToString((byte[]) entry.getValue()));
});
});`
}
public static void convertByteToBase64(List<Map<String, Object>> listOfValue) {
listOfValue.stream().parallel()
.forEach(map -> map.forEach((key,value)->{
if(value instanceof byte[]) {
map.put(key, Base64.getEncoder().encodeToString((byte[])value)) ;
}
}));
}
I have a class named ConfigKey
public class ConfigKey {
String code;
String key;
String value;
//omit setter and getter
}
I want to convert List<ConfigKey> to Map<String, Map<String, Object>>, here is my method definition
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode,
Collectors.toMap(ConfigKey::getKey, ConfigKey::getValue)));
}
however I want to do some changes, for each ConfigKey put another key to the map, e.g.
{ "code": "code1","key", "key1", "value": "value1"}
to Map
{"code1": {"key1":"value1", "prefix_key1": "value1" }
is there any API to do it like bellow:
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode,
Collectors.toMap("prefix_" + ConfigKey::getKey, ConfigKey::getValue))
Collectors.toMap(ConfigKey::getKey, ConfigKey::getValue)));
}
You can make use of the Collector.of() factory method, which allows you to create your own collector:
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode, Collector.of(
HashMap::new, (m, c) -> {
m.put(c.getKey(), c.getValue());
m.put("prefix_" + c.getKey(), c.getValue());
}, (a, b) -> {
a.putAll(b);
return b;
}
)));
}
But honestly that seems a bit messy, and maybe a normal loop would've been better. The streams intention was to provide an api which does things in a more readable manner, but when you have to hackaround that construct, by introducing some extremely unreadable logic then it is almost always the better option to just do it the old way:
public Map<String, Map<String, Object> convert (List<ConfigKey> list) {
Map<String, Map<String, Object>> map = new HashMap<>();
for (ConfigKey ck : list) {
Map<String, Object> inner = map.computeIfAbsent(ck.getCode(), k -> new HashMap<>());
inner.put(ck.getKey(), ck.getValue());
inner.put("prefix_" + ck.getKey(), ck.getValue());
}
return map;
}
You can first add the new entries to the map and then group them:
private Map<String, Map<String, Object>> convert(List<ConfigKey> list) {
new ArrayList<>(list).stream().map(configKey -> new ConfigKey(configKey.getCode(), "prefix_" + configKey.getKey(), configKey.getValue())).forEachOrdered(list::add);
return list.stream().collect(Collectors.groupingBy(ConfigKey::getCode,
Collectors.toMap(ConfigKey::getKey, ConfigKey::getValue)));
}
I cloned the list (in order to prevent ConcurrentModificationException), then changed the keys to the "new" ones (with map) and added them to the original list - forEachOrdered(list::add).
Because the 'code' field was not changed, both entries will use it which results in 2 entries in the map
This question already has answers here:
How does recursion work with Java 8 Stream?
(2 answers)
Closed 4 years ago.
I have the following structure in HashMaps (not all the values are maps (see id) and there are more levels embedded):
{
"id":"0",
"name":{
"title":"Mr.",
"personName": {
"firstName":"Boot",
"familyName":"Strap"
}
}
}
Can I flatten it with Java 8 stream like
{
"id":"0",
"name.title":"Mr.",
"name.personName.firstName":"Boot",
"name.personName.familyName":"Strap"
}
?
Streams does not do recursive computation, you may use classic way like
static Map<String, String> flatten(Map<String, Object> map, String prefix) {
prefix = prefix.isEmpty() ? prefix : prefix + ".";
Map<String, String> res = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Map) {
res.putAll(flatten((Map) entry.getValue(), prefix + entry.getKey()));
} else {
res.put(prefix + entry.getKey(), entry.getValue().toString());
}
}
return res;
}
Stream way will look like
static Map<String, String> flatten(Map<String, Object> map, String p) {
final String prefix = p.isEmpty() ? p : p + ".";
return map.entrySet()
.stream()
.map(e -> {
if (e.getValue() instanceof Map) {
return (Set<Map.Entry<String, String>>) flatten((Map) e.getValue(), prefix + e.getKey()).entrySet();
} else {
return Map.of(prefix + e.getKey(), e.getValue().toString()).entrySet();
}
})
.flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
And use as
public static void main(String[] args) {
Map<String, Object> map = Map.of("id", 0, "name", Map.of("title", "Mr.", "personName", Map.of("firstName", "Boot", "familyName", "Strap")));
Map<String, String> res = flatten(map, "");
res.entrySet().forEach(System.out::println);
}
name.title=Mr.
name.personName.familyName=Strap
name.personName.firstName=Boot
id=0
It is pretty simple not using Streams, but use recursion:
public static Map<String, String> flatten(Map<String, Object> map) {
return flatten("", map, new LinkedHashMap<>());
}
private static Map<String, String> flatten(String parent, Map<String, Object> map, Map<String, String> res) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = parent.isEmpty() ? entry.getKey() : parent + '.' + entry.getKey();
if (entry.getValue() instanceof Map)
flatten(key, (Map<String, Object>)entry.getValue(), res);
else
res.put(key, String.valueOf(entry.getValue()));
}
return res;
}
P.S. I know that string concatenation in loop is not good (it is better to use StringBuilder or even Deque), but I use String to make this example more clear.
I have the following code:
private Map<String, Object> flatten(Map<String, Object> source) {
Map<String, Object> result = new LinkedHashMap<>();
source.forEach((k, v) -> {
if (v instanceof Map) {
Map<String, Object> subMap = flatten((Map<String, Object>) v);
for (String subkey : subMap.keySet()) {
result.put(k + "." + subkey, subMap.get(subkey));
}
} else result.put(k, v);
});
return result;
}
The above code flattens a given Map
Ex:
{
"hello": {
"hi": {
"hola": 1
}
}
}
to
{
"hello.hi.hola": 1
}
I would like to use Java8 streams and implement the same logic, How can I do that?
I was going to post something similar to #John Bollinger's answer. Instead I'll offer a somewhat cleaner version using StreamEx:
static Map<String, Object> flatten(Map<?, ?> map) {
return flatten("", map)
.mapKeys(k -> k.substring(1))
.toMap();
}
static EntryStream<String, Object> flatten(String key, Object value) {
if (value instanceof Map) {
return EntryStream.of((Map<?, ?>)value)
.flatMapKeyValue((k, v) -> flatten(key + "." + k, v))
.chain(EntryStream::of);
} else {
return EntryStream.of(key, value);
}
}
Nope, the Stream API has not been designed to go with the recursion well. There are not much possibilities to "enhance" your solution using Stream API nor refactor. From the definition of Stream API from its package info is obvious that their usage has to be non-interfering, stateless and without side-effects - thus I can't imagine how you would achieve this using this API.
I find the only possible usage amending the inner block of the if-condition which brings no real benefit:
if (v instanceof Map) {
Map<String, Object> subMap = flatten((Map<String, Object>) v);
subMap.keySet().stream().forEach(subkey -> result.put(k + "." + subkey, subMap.get(subkey)));
} else result.put(k, v);
The task you present is not well suited to implementation via streams. What you already have is concise and clean. Recursion is a particular problem because lambdas cannot refer to themselves or directly recurse.
However, you don't have to express stream operations in terms of lambdas. You can express them in terms of instances of ordinary classes, too, and such instances can recurse. Thus, you might rewrite your flatten() method like so:
private Map<String, Object> flatten(Map<String, Object> source) {
Function<Entry<?, ?>, Stream<Entry<String, Object>>> flattener
= new Function<Entry<?, ?>, Stream<Entry<String, Object>>>() {
public Stream<Entry<String, Object>> apply(Entry<?, ?> e) {
String k = e.getKey().toString();
Object v = e.getValue();
if (v instanceof Map) {
return ((Map<?, ?>) v).entrySet()
.stream()
.flatMap(this)
.map(n -> new SimpleEntry<>(k + "." + n.getKey(), n.getValue()));
} else {
return Stream.of(new SimpleEntry<>(k, v));
}
}
};
return source.entrySet()
.stream()
.flatMap(flattener)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
The main body of that, at the end, is a pretty simple stream pipeline. The magic happens in the flatMap() step, with the provided flattening Function. It, too, is written in terms of stream operations, and it recurses by passing itself to its own invocation of Stream.flatMap().
But I cannot imagine why anyone would prefer this mess to the code you started with.
Suppose I have having Json response like this:
{
"status": true,
"data": {
"29": "Hardik sheth",
"30": "Kavit Gosvami"
}
}
I am using Retrofit to parse Json response. As per this answer I will have to use Map<String, String> which will give all the data in Map. Now what I want is ArrayList<PojoObject>.
PojoObject.class
public class PojoObject {
private String mapKey, mapValue;
public String getMapKey() {
return mapKey;
}
public void setMapKey(String mapKey) {
this.mapKey = mapKey;
}
public String getMapValue() {
return mapValue;
}
public void setMapValue(String mapValue) {
this.mapValue = mapValue;
}
}
What is the best way to convert a Map<key,value> to a List<PojoObject>?
If you can expand your class to have a constructor taking the values as well:
map.entrySet()
.stream()
.map(e -> new PojoObject(e.getKey(), e.getValue()))
.collect(Collectors.toList());
If you can't:
map.entrySet()
.stream()
.map(e -> {
PojoObject po = new PojoObject();
po.setMapKey(e.getKey());
po.setMapValue(e.getValue());
return po;
}).collect(Collectors.toList());
Note that this uses Java 8 Stream API.
Looks like Java has exact POJO Map.Entry like you want. Hence, you can extract the entry set from map and iterate over the entry set like below or you can further convert the set to list like in next snippet and continue with your processing.
//fetch entry set from map
Set<Entry<String, String>> set = map.entrySet();
for(Entry<String, String> entry: set) {
System.out.println(entry.getKey() +"," + entry.getValue());
}
//convert set to list
List<Entry<String, String>> list = new ArrayList(set);
for(Entry<String, String> entry: list) {
System.out.println(entry.getKey() +"," + entry.getValue());
}
Try this
List<Value> list = new ArrayList<Value>(map.values());
Or
hashMap.keySet().toArray(); // returns an array of keys
hashMap.values().toArray(); // returns an array of values
Should be noted that the ordering of both arrays may not be the same.
or
hashMap.entrySet().toArray();
You can use this method to convert map to list
List<PojoObject> list = new ArrayList<PojoObject>(map.values());
Assuming:
Map <Key,Value> map;
ArrayList<Map<String,String>> list = new ArrayList<Map<String,String>>();
this may be the best way.