wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Schema mapping with Java functional interfaces


Mapping one data structure into another is a never ending chore since COBOL's MOVE CORRESPONDING. One to one mappings are trivial, onnce computation is needed, clean code can become messy, really fast

Map with Functions

We will use the following, simplified, scenario with source and target formats:

{
 "FirstName" : "Peter",
 "LastName" : "Pan",
 "DoB" : "1873-11-23",
 "ToC" : "accepted",
 "Marketing" : "no"
}

Desired Result:

{
 "fullname" : "Peter Pan",
 "birthday" : "1873-11-23",
 "gdpr" : true
}

In our case only DoB has a simple mapping to birthday all others need computation or are dropped. So to keep code clean we will use a map with mapping functions, so each computation can be in its own method. The defaults 1:1 and drop functions get defined first.

final Map<String, Function<JsonObject, JsonObject>> functionList = new HashMap<>();

Function<JsonObject, JsonObject> simple(final String fieldNameIn, final String fieldNameOut) {
 return in -> new JsonObject().put(fieldNameOut, in.getValue(fieldNameIn));
}

Function<JsonObject, JsonObject> drop() {
 return in -> new JsonObject();
}

Each of the functions returns an Json object that only returns a value for the one field it gets called for. We will use a collector to aggregate the values. Since we are planning to use streams and functional interfaces, we need a helper class.

class MapHelper() {
 JsonObject source;
 Function<JsonObject, JsonObject> mapper;
 JsonObject apply() {
  return this.mapper.apply(this.source);
 }
}

MapHelper getMapHelper(final JsonObject source, final Map.Entry<String, Object> incoming) {
    MapHelper result = new MapHelper();
    result.source = source;
    result.mapper = this.functionList.getOrDefault(incoming.getKey(), drop());
    return result;
  }

Since each function will return some JSON, that needs to be merged together, we use a Java Collector to accumulate the values

The Collector interface provides the static function Collector.of that is suitable for our purpose:

Collector<MapHelper, JsonObject, JsonObject> myCollector() {
 return Collector.of(
  JsonObject::new,
  (internal, incoming) -> internal.mergeIn(incoming.apply()),
  (part1, part2) -> part1.mergeIn(part2)
 );
}

Next stop are the additional functions for the gdpr and the fullname fields and the population of our functionList A few small functions will do.


Function<JsonObject, JsonObject> gdpr() {
 return in -> in.getString("ToC").equals("accepted") && !in.getString("Marketing").equals("unknown");
}


Function<JsonObject, JsonObject> fullname() {
 return in -> {
  final String fullname = in.getString("FirstName")+" "+in.getString("LastName");
  return new JsonObject().put("fullname", fullname);
 }
}


void populateMapper() {
 functionList.put("DoB", simple("DoB","birthday"));
 functionList.put("gdpr", grpr());
 functionList.put("LastName", fullname());
}

With all this in place we now can setup the final function that excutes our actual mapping operation;

JsonObject transform(final JsonObject incoming) {
 populateMapper();
 return incoming.stream()
 .map(e -> getMapHelper(incoming, e))
 .collect(myCollector());
}

What have you seen: usage of Java Streams, custom collector and functional interfaces to create clean mapping code. Be aware that you need better checks in your functions, omitted here for readability

As usual YMMV


Posted by on 28 November 2020 | Comments (0) | categories: Java

Comments

  1. No comments yet, be the first to comment