UserSummaryCollector.java

package com.wilzwert.myjobs.core.domain.model.user.collector;


import com.wilzwert.myjobs.core.domain.model.job.JobState;
import com.wilzwert.myjobs.core.domain.model.job.JobStatus;
import com.wilzwert.myjobs.core.domain.model.job.JobStatusMeta;
import com.wilzwert.myjobs.core.domain.model.user.User;
import com.wilzwert.myjobs.core.domain.model.user.UserSummary;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;

/**
 * @author Wilhelm Zwertvaegher
 * Date:03/06/2025
 * Time:09:51
 */

public class UserSummaryCollector implements Collector<JobState, Map<JobStatus, List<JobState>>, UserSummary> {


    private final User user;

    private int jobsCount = 0;

    public UserSummaryCollector(User user) {
        this.user = user;
    }

    @Override
    public Supplier<Map<JobStatus, List<JobState>>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiConsumer<Map<JobStatus, List<JobState>>, JobState> accumulator() {
        return (statusesToCount, jobState) -> {
            jobsCount++;
            statusesToCount.computeIfAbsent(jobState.status(), jobStatus -> new ArrayList<>() ).add(jobState);
        };
    }

    @Override
    public BinaryOperator<Map<JobStatus, List<JobState>>> combiner() {
        return (set1, set2) -> { throw new UnsupportedOperationException("Parallel processing is not supported"); };
    }

    @Override
    public Function<Map<JobStatus, List<JobState>>, UserSummary> finisher() {
        return statusToStates -> {
            // FIXME : it may actually be better to loop through statusToStates
            // than to use numerous streams

            // map statuses to jobs count
            Map<JobStatus, Integer> statusToCount = statusToStates.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size()));

            // compute active, inactive jobs counts
            int activeJobsCount = statusToStates.entrySet().stream()
                   .filter(e -> JobStatus.activeStatuses().contains(e.getKey()))
                   .mapToInt(e -> e.getValue().size()).sum();
            int inactiveJobsCount = jobsCount - activeJobsCount;

            // compute usable filters
            Set<JobStatusMeta> filters = new HashSet<>();
            if(statusToStates.entrySet().stream().anyMatch(e -> JobStatus.activeStatuses().contains(e.getKey()))) {
                filters.add(JobStatusMeta.ACTIVE);
            }
            if(statusToStates.entrySet().stream().anyMatch(e -> JobStatus.inactiveStatuses().contains(e.getKey()))) {
                filters.add(JobStatusMeta.INACTIVE);
            }

            long lateJobsCount = statusToStates.entrySet().stream()
                    .filter(e -> JobStatus.activeStatuses().contains(e.getKey()))
                    .flatMap(jobStatusListEntry -> jobStatusListEntry.getValue().stream())
                    .filter(state -> user.isJobLate(state.statusUpdatedAt()))
                    .count();

            if(lateJobsCount > Integer.MAX_VALUE) {
                lateJobsCount = Integer.MAX_VALUE;
            }

            if(lateJobsCount > 0) {
                filters.add(JobStatusMeta.LATE);
            }

            return new UserSummary(jobsCount, activeJobsCount, inactiveJobsCount, (int)lateJobsCount, statusToCount, filters);
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of();
    }
}