I have two JSON and each JSON I am loading it in a Map. Just to make example simpler, I have shortened the JSON.
First JSON
{
"abc": "2",
"plmtq": "hello+world",
"lndp": "def",
"yyt": "opi"
}
I am loading this JSON in a map as key:value
pair. Here abc
is the key and 2
is the value. Let's call this mapA
. This is a String to String map.
Second JSON
{
"count": {
"linkedTo": "abc"
},
"title": {
"linkedTo": "plmtq",
"decode": "true"
},
"cross": {
"linkedTo": "lndp"
},
"browse": {
"linkedTo": "lndp"
}
}
I am also loading this JSON in a map. Let's call this mapB
. And this is Map<String, Map<Tag, String>>
. So in the mapB
, key is count
and value is another map in which key is linkedTo
Tag enum and value is abc
String. Similarly for others. I am also open in having a different format for this if it makes this problem simpler.
Here Tag
is an enum class with these values. For now it only has two value but in general it has around 7-8 values.
public enum Tag {
linkedTo, decode;
};
For example: Here linkedTo
means value of abc
will go into count
variable. Similarly value of plmtq
goes into title
variable but with URL decoded as decode
field is present and true.
Problem Statement:
Now I need to use these two maps and make a new map which will look like this and it will be String and String as well.
count=2
title=hello world // as you can see this got URL decoded with UTF-8 format.
cross=def
browse=def
yyt=opi
So each enum in the Tag
class has special meanings and I need to perform certain operations accordingly. Right now it has two but in general it has 7-8. What is the best and efficient way to solve this problem?
private static Map<String, String> generateMap(final Map<String, String> mapA,
final Map<String, Map<Tag, String>> mapB) {
Map<String, String> newMap = new HashMap<>();
for (Entry<String, Map<Tag, String>> entry : mapB.entrySet()) {
String eventTag = entry.getKey();
// I am confuse how to proceed further
}
return newMap;
}
Update:-
After discussion with dimo, we came up this new JSON design for second map:
SECOND JSON
{
"abc": {
"renameTo": "count"
},
"plmtq": {
"renameTo": "title",
"decode": "true"
},
"lndp": {
"renameTo": ["cross", "browse"],
"decode": "true"
}
}
It is just reverted format basically. So in the newMap
key will be count
and it's value will be value of abc
. Similarly for lndp
case, in the newMap
key will be cross
and browse
and it's value will be def
with URL decoded.
I'm using Java 7 for most of this answer, but there's some Java 8 comments at the bottom.
There's two key insights I think are important to start with:
mapA
and mapB
don't, making it difficult to reason about the problems you're trying to solve.I'll try to apply both of these concepts here.
Convert the JSON into a meaningful and useful data structure (i.e. not just a bunch of Map
objects). Your second JSON has some clear structure that isn't easy to work with as nested maps. Rather than trying to create complex algorithms to work with convoluted data, create objects that allow you to represent and interact with your data meaningfully.
Consider representing your second JSON as a Map<String, Transformation>
, where the Transformation
class looks like so:
public class Transformation {
/** Applies one or more transformations to the provided entry. */
public Entry<String, String> apply(Entry<String, String> e) {
// ...
}
}
What does apply()
do? We'll get to that. For now it's good enough to have the class structure, because now we can write a simply function to do the processing you need.
With helpful data structures your task at hand becomes simple. Consider something like this (notice the clearer function and variable names):
private static Map<String, String> decodeMap(
Map<String, String> encodedData,
Map<String, Transformation> transformations) {
Map<String, String> decodedData = new HashMap<>();
for (Entry<String, String> e : encodedData.entrySet()) {
Transformation t = transformations.get(e.getKey());
if (t == null) {
t = Transformation.NO_OP; // just passes the entry through
}
Entry<String, String> decoded = t.apply(e);
decodedData.put(decoded.getKey(), decoded.getValue());
}
return decodedData;
}
This is a pretty simple method; we iterate over every entry, apply a (possibly no-op) transformation, and construct a map of those transformed entries. Assuming our Transformation
class does its job it should be fairly apparent that this method works.
Notice that the keys of transformations
are the encoded map's keys, not the decoded keys. This is a lot easier to work with, so ideally you'd invert your JSON to refect the same structure, but if you can't do that you just need to flip them in code before building the Transformation
objects.
Now create the private business logic to encode your transformations inside the Transformation
class. This might looks something like:
public class Transformation {
public static final Transformation NO_OP =
new Transformation(ImmutableMap.of());
private final Map<String, String> tasks;
public Transformation(Map<String, String> tasks) {
// could add some sanity checks here that tasks.keySet() doesn't
// contain any unexpected values
this.tasks = tasks;
}
public Entry<String, String> apply(Entry<String, String> e) {
String key = e.getKey();
String value = e.getValue();
for (Entry<String, String> task : tasks.entrySet()) {
switch (task.getKey()) {
case "linkedTo":
// this assumes we've inverted the old/new mapping. If not pass
// the new key name into the constructor and use that here instead
key = task.getValue();
break;
case "decode":
if (Boolean.valueOf(task.getValue())) {
// or whichever decoding function you prefer
value = URLDecoder.decode(value);
}
break;
// and so on
default:
throw new IllegalArgumentException(
"Unknown task " + task.getKey());
}
}
return Maps.immutableEntry(key, value);
}
The Transformation
class can handle as many tasks as you'd like, and no other code needs to be aware of the specifics.
The last step is to parse your JSON into this Map<String, Transformation>
structure. I'll leave that up to you as it depends on your JSON parser of choice, however I suggest using Gson.
Your requirement that the lndp
entry be transformed into two separate entries with the same value is an odd one. It smells strongly to me like an x-y problem - you're seeking help making this complex transformation logic work rather than exploring whether the underlying premise itself is flawed. Consider exploring that in a separate question where you spell out what you're starting with, where you're trying to go, and why your current solution seems best. Then people can offer suggestions on the strategy itself.
That said, we can still apply my second bullet point above to this problem. Rather than attempting to define transformation logic that supports multi-entry relationships, lets add a post-processing step to the transformation. Define a separate data structure that encodes which entries (in the new map) should be duplicated, e.g.
{
"cross": ["browse"],
...
}
Then, decodeMap()
would look something like this:
private static Map<String, String> decodeMap(
Map<String, String> encodedData,
Map<String, Transformation> transformations,
Map<String, List<String>> duplications) {
// ... existing method body
for (Entry<String, List<String>> e : duplications) {
String value = checkNotNull(decodedData.get(e.getKey()));
for (String newKey : e.getValue()) {
decodedData.put(newKey, value);
}
}
return decodedData;
}
This is another example of separating the problem into smaller parts, rather than trying to solve everything in one fell swoop. You could even split these tasks into separate methods, so that decodeMap()
doesn't itself become too complex.
You may notice that the Transformation
class looks a lot like a Function
- that's because it essentially is one. You could add implements Function<Entry<String, String>, Entry<String, String>>
(or in Java 8 implements UnaryOperator<Entry<String, String>>
) and be able to use Transformation
just like any other Function
.
In particular this would allow you to make Transformation
an interface and compose simple Transformation
objects together, rather than define one massive type responsible for every task. You'd have a dedicated subclass of Transformation
for each desired task, e.g.:
public RenameKeyTransformation implements Transformation {
private final renameTo;
public RenameKeyTransformation(String renameTo) {
this.renameTo = checkNotNull(renameTo);
}
public Entry<String, String> apply(Entry<String, String> e) {
return Maps.immutableEntry(renameTo, e.getValue());
}
}
And so on. Then you can compose them with Functions.compose()
(or Java 8's Function.andThen()
) to create a compound function that applies all the desired transformations. For example this:
Function<Entry<String, String>, Entry<String, String>> compound =
Functions.compose(
new RenameKeyTransformation("plmtq"),
new UrlDecodeValue());
returns a new function that will apply both a rename-key operation and a url-decode-value operation to a given entry. You can repeatedly chain functions to apply as many transformations as you need.
The first Transformation
object may be simpler to conceptualize (and it's a perfectly reasonable solution), but this second idea of composing small functions is taking the encapsulation idea mentioned above to another level, isolating each individual transformation task from one another and making it easy to add further transformations as you go. This is an example of the strategy pattern, which is an elegant way to decouple the data you're processing from the exact processing steps to be carried out.