Search code examples
javadictionaryhashmapguava

How to use two maps and make another map?


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.


Solution

  • 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:

    • Use clear class, method, and variable names - names should convey meaning and purpose, whereas identifiers like mapA and mapB don't, making it difficult to reason about the problems you're trying to solve.
    • Separate your problem into smaller parts; solve those parts in isolation, then put them together.

    I'll try to apply both of these concepts here.


    1. 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.

    2. 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.

    3. 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.

    4. 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.