Better way of logging of excluded results with Stream API

Viewed 1016

I'm wondering is there a way to rewrite code like this

public static void main(String[] args) {
    final List<String> dataCollection = Collections.emptyList();
    final Set<String> someValues = new HashSet<>();
    final Iterator<String> iterator = dataCollection.iterator();
    while (iterator.hasNext()) {
        final String dataItem = iterator.next();
        // imagine some calculations
        String calculatedData = dataItem;
        if (!someValues.contains(calculatedData)) {
            logger.error("Skipped data {} because of ...#1", dataItem);
            iterator.remove();
            continue;
        }

        for (char element : dataItem.toCharArray()) {
            // imagine some other calculations
            if (element > 100) {
                logger.error("Skipped data {} because of ...#2", dataItem);
                iterator.remove();
                break;
            }
        }
    }
}

with Stream API so that excluded after filters element were logged. peek() doesn't work in this case, because it either perform action with every element before filter or after it with remaining items.

So far I managed to design it with logging inside lambda, but it seems verbose, awkward and like side effect. We can wrap it inside some method, but it will only hide that code a bit

public static void main(String[] args) {
    final List<String> dataCollection = Collections.emptyList();
    final Set<String> someValues = new HashSet<>();
    final Iterator<String> iterator = dataCollection.iterator();

    dataCollection.stream()
            .filter(byCondition1(someValues))
            .filter(byCondition2())
            .collect(Collectors.toList());
}

private static Predicate<String> byCondition1(Set<String> someValues) {
    return dataItem -> {
        final boolean remain = someValues.contains(dataItem);
        if (!remain) {
            logger.error("Skipped data {} because of ...#1", dataItem);
        }
        return remain;
    };
}

private static Predicate<String> byCondition2() {
    return dataItem -> {
        for (char element : dataItem.toCharArray()) {
            // imagine some other calculations
            if (element > 100) {
                logger.error("Skipped data {} because of element {}...#2", dataItem, element);
                return false;
            }
        }
        return true;
    };
}

I hope that there is a better way.

2 Answers

Assuming that skipping is an extraordinary condition, this may be feasible

List<String> dataCollection = Arrays.asList("FOO", "hello", "VALID", "123", "BAR", "bla");
Set<String> someValues = new HashSet<>(Arrays.asList("FOO", "BAR"));

Predicate<String> firstPredicate  = string -> !someValues.contains(string);
Predicate<String> secondPredicate = string -> string.chars().noneMatch(c -> c>100);

List<String> result;

if(!logger.isLoggable(Level.WARNING)) {
    result = dataCollection.stream()
           .filter(firstPredicate)
           .filter(secondPredicate)
           .collect(Collectors.toList());
}
else {
    Map<Boolean, List<String>> map = dataCollection.stream()
        .collect(Collectors.partitioningBy(firstPredicate));
    if(!map.get(false).isEmpty())
        logger.log(Level.WARNING, "Skipped data {0} because of ...#1", map.get(false));
    map = map.get(true).stream()
        .collect(Collectors.partitioningBy(secondPredicate));
    if(!map.get(false).isEmpty())
        logger.log(Level.WARNING, "Skipped data {0} because of ...#2", map.get(false));
    result = map.get(true);
}

This code uses the standard java.util.logging API and will not perform any logging related operation if logging for the specified level is disabled. On the other hand, if enabled and encountering items to skip, it will report all of them at once per condition rather than flooding the log with an entry per element. Using the sample data shown above and the default log handler, it printed

Sep 26, 2017 11:00:46 AM LoggingExample main
WARNUNG: Skipped data [FOO, BAR] because of ...#1
Sep 26, 2017 11:00:46 AM LoggingExample main
WARNUNG: Skipped data [hello, bla] because of ...#2

The result being [VALID, 123], whether logging for that level is enabled or not.

First of all, I think the code in OP is not too bad. Secondly, there are still a bit of improvements we can do by the original code:

final Predicate<String> loggingFilter = dataItem -> {
    final String calculatedData = dataItem; // imagine some calculations
    if (!someValues.contains(calculatedData)) {
        logger.error("Skipped data {} because of ...#1", dataItem);
        return false;
    }         
    final OptionalInt element = dataItem.chars().filter(ch -> ch > 100).findAny();
    if (element.isPresent()) {
        logger.error("Skipped data {} because of element {}...#2", dataItem, element.getAsInt());
        return false;
    }
    return true;
};

dataCollection.stream().filter(loggingFilter).collect(Collectors.toList());

I'm not sure if it's: "verbose, awkward and like side effect" to you or anyone else. To me, there is no duplicated code or side effect. and that's pretty much all we can by any language, except some languages may provide chain operation API to remove if(...); if(...).

Related