Is there a way to stream().collect() 1 item into 2 groups?

Viewed 116

My goal is to group classes within a package and subpackages into Map<ClassAnnotationType, List<Class> map. @usage e.g map.get(ClassAnnotationType.RestController)

What I've done:

  1. Get all classes within a package and subpackages. Stack Overflow's question

ClassAnnotationType

public enum ClassAnnotationType {
  RestController,
  Service,
  Unknown
}

List of Class Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RestController{
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {
}

This is how the search tree would work

  /**
   * .class .class package                              package
   *                 / \ (first iteration)                / \ (second iteration)
   *        .class .class package                 .class .class package
   *                        / \ (first iteration)                 / \ (second iteration)
   */

This is the line of code I wish to change

  private Map<Boolean, List<String>> getResources(String packageName) {
    InputStream inputStream = ClassLoader.getSystemClassLoader()
            .getResourceAsStream(packageName.replaceAll("[.]", "/"));
    BufferedReader bufferedReader = new BufferedReader(
            new InputStreamReader(inputStream));
    return bufferedReader.lines()
            .map(i -> packageName.concat("." + i))
            .collect(Collectors.partitioningBy(i -> i.endsWith(".class")));
  }

into something like this

.collect(
        Collectors.groupingBy(i -> {
          for (Annotation annotation : i.getAnnotations()) {
            // add the Class to the key.
          }
        }));

My expected result: Collectors.groupingBy() can add 1 item class Hello into 2 different groups e.g RestController, Service

My actual result: 1 item for 1 group.

Current pre-solution

.collect(i -> {
  if (i.isAnnotationPresent(RestController.class)) {
    return ClassAnnotationType.RestController;
  }
  return ClassAnnotationType.Unknown; // i wish not to do this.
});

This created problems:

  1. 1 class only can exists in 1 group.
  2. Due to the nature of ClassLoader.getSystemClassLoader().getResourceAsStream() it will return class class package within a package. The current code has to list the tree first and map it into String before type casting it to Class using the Class.forName() method. In other words, the only viable way is to do it in three steps. a. get all the class within a package and subpackages with recursion to a List<String> b. type cast it to List<Class> c. map it to Map<ClassAnnotationType, List<Class>.
  3. All of the classes need to be mapped into a key, including the class without annotation return ClassAnnotationType.Unknown
2 Answers

You can do this with a custom Collector, with Collector.of. Perhaps something like:

Collector.of(
    // Supplier.
    HashMap::new,

    // Accumulator.
    (map, i) -> {
          // Your code. You can put whatever you like into the map,
          // so you can put something in for multiple keys.
          for (Annotation annotation : i.getAnnotations()) {
            // add the Class to the key.
          }
    },

    // Combiner.
    (map1, map2) -> { map1.putAll(map2); return map1; }
)

Here's one way to solve it, but perhaps better explained with a simpler and reproducible example.

Suppose you had the following two Integer lists:

List<Integer> numbers = List.of(2, 3);
List<Integer> multiples = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);

And you wanted to map each multiple to each of its divisors, grouping those that don't have a corresponding divisor under key -1. A number like 6 would have to show up in the mapping for both 2 and 3.

Here's one solution for it:

Map<Integer, List<Integer>> map = numbers.stream()
    .map(div -> Map.entry(div, multiples.stream()
            .collect(Collectors.partitioningBy(e -> 0 == e % div))))
    .flatMap(partitions -> List.of(
            Map.entry(partitions.getKey(), partitions.getValue().get(true)), 
            Map.entry(-1, partitions.getValue().get(false))
        ).stream())
    .collect(Collectors.groupingBy(
            Entry::getKey, 
            Collectors.flatMapping(e -> e.getValue().stream(), 
                                   Collectors.toList())));

Which will produce

{-1=[1, 3, 5, 7, 9, 11, 1, 2, 4, 5, 7, 8, 10, 11], 
  2=[2, 4, 6, 8, 10, 12], 
  3=[3, 6, 9, 12]}

Now, there's clearly a problem with the -1 values, which are the union of all values that failed to be divided by any number.
I didn't see a clean way to resolve it but by using the following cleanup code:

Set<Integer> foundDivisors = new HashSet<>(map.getOrDefault(-1, new ArrayList<>()));
map.entrySet().stream()
    .filter(div -> !div.getKey().equals(-1))
    .forEach(entry -> foundDivisors.removeAll(entry.getValue()));
map.replace(-1, new ArrayList<>(foundDivisors));

And the result is map has the expected values:

{-1=[1, 5, 7, 11], 
  2=[2, 4, 6, 8, 10, 12], 
  3=[3, 6, 9, 12]}

You can use the same logic with your code. I couldn't test it, but it would roughly look like this:

//maps enum to scanned annotation
enum ClassAnnotationType {
    REST_CONTROLLER(RestController.class), 
    SERVICE(Service.class), 
    UNKNOWN(null)
    // constructor, etc.
}


Map<ClassAnnotationType, List<Class<?>>> map = Arrays.stream(ClassAnnotationType.values())
    .filter(en -> en != ClassAnnotationType.UNKNOWN)
    .map(type -> Map.entry(type, yourClassList.stream()
            .collect(Collectors.partitioningBy(cls -> 
                cls.isAnnotationPresent(type.getAnnotationClass())))))
    .flatMap(partitions -> List.of(Map.entry(partitions.getKey(), partitions.getValue()
            .get(true)), Map.entry(ClassAnnotationType.UNKNOWN,
                    partitions.getValue().get(false))).stream())
    .collect(Collectors.groupingBy(Entry::getKey, 
             Collectors.flatMapping(e -> e.getValue().stream(), 
                     Collectors.toList())));

And then proceed with the removal of the value set for ClassAnnotationType.UNKNOWN as done above.

Related