wissel.net

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

By Date: November 2020

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

Streams and Functional programming in Java


I'm late to the party embracing Streams and functional interfaces in Java. Using them for a while taught me the beauty and how things fit together nicely

Moving parts

  • At the beginning a class implementing the Stream interface emits items, that can be manipulated using map and filter operationf
  • The map and filter operations are supported by the Interfaces in java.util.function (we get to the samples later)
  • At the end the result gets "collected", in its simplest form using .forEach or, more sophisticated using a Collector with many ready baked options

What's the big deal?

short answer: clean, terse and clutter free code.

long answer: an example. Lets say you have a mammal class which gets subclassed by cat and dog (and others). You have a collection of these mamals and need to extract all dogs over weight 50. Weight is not a property of mammal. There might be null values in your collection. Classic code would look like this:

List<Dog> getHeavyDogs(final List<Mammal> mammals) {
    List<Dog> result = new ArrayList<>();
    for (int i = 0; i < mammals.size(); i++) {
      Mammal mammal = mammals.get(i);
      if (mammal != null) {
        if (mammal instanceof Dog && ((Dog) mammal).weight() > 50) {
          result.add((Dog) mammal);
        }
      }
    }
    return result;
  }

We all seen this type of code. In a functional and stream style this would look different. We have a little duck typing going on here. When a method looks like a functional interface, it can be used as this function. E.g. a method that takes one value and returns a boolean can be used as a Predicate, which comes in handy for filter operations. Another nifty syntax: you can address methods, both static and instance using the :: (double colon) syntax. So when you could use a lambda x -> this.doSomething(x) you can simply write this::doSomething and the compiler will sort it out (System.out::println anyone?)

Rewriting our example turns out like this:

List<Dog> getHeavyDogs(final List<Mammal> mammals) {
    return mammals.stream()
        .filter(Objects::nonNull)
        .filter(Dog.class::isInstance)
        .map(Dog.class::cast)
        .filter(dog -> dog.weight() > 50)
        .collect(Collectors.toList());
  }

Much cleaner, and it's Java without curly brackets. You could even skip the line with Objects::nonNull since class::isInstance always returns false for null values. I kept it here to introduce Objects, a neat helper class.

Lets break it down:

  • mammals.stream() provides a stream of mammal instances
  • .filter(Objects::nonNull) uses a static method to clear out null values
  • .filter(Dog.class::isInstance) only items that are Dog instances get through
  • .map(Dog.class::cast) cast the mammal into a dog. You are not limited by casts, any function can be used
  • .filter(dog -> dog.weight() > 50) filter out the heavy dogs
  • .collect(Collectors.toList()); put the result into a list

The DominoJNX API makes heavy use of streams, but that's another story for another time. There's more to functional interfaces, streams and collectors (sums, groups, computations etc), so never stop learning!

As usual YMMV


Posted by on 06 November 2020 | Comments (3) | categories: Java