nu.yona.server.analysis.service.ActivityService.java Source code

Java tutorial

Introduction

Here is the source code for nu.yona.server.analysis.service.ActivityService.java

Source

/*******************************************************************************
 * Copyright (c) 2016, 2018 Stichting Yona Foundation This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *******************************************************************************/
package nu.yona.server.analysis.service;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import nu.yona.server.analysis.entities.ActivityCommentMessage;
import nu.yona.server.analysis.entities.DayActivity;
import nu.yona.server.analysis.entities.DayActivityRepository;
import nu.yona.server.analysis.entities.IntervalActivity;
import nu.yona.server.analysis.entities.WeekActivity;
import nu.yona.server.analysis.entities.WeekActivityRepository;
import nu.yona.server.analysis.service.IntervalActivityDto.LevelOfDetail;
import nu.yona.server.goals.entities.Goal;
import nu.yona.server.goals.service.GoalDto;
import nu.yona.server.goals.service.GoalService;
import nu.yona.server.messaging.entities.BuddyMessage.BuddyInfoParameters;
import nu.yona.server.messaging.entities.Message;
import nu.yona.server.messaging.entities.MessageRepository;
import nu.yona.server.messaging.service.BuddyMessageDto;
import nu.yona.server.messaging.service.MessageDto;
import nu.yona.server.messaging.service.MessageService;
import nu.yona.server.properties.YonaProperties;
import nu.yona.server.subscriptions.entities.UserAnonymized;
import nu.yona.server.subscriptions.service.BuddyDto;
import nu.yona.server.subscriptions.service.BuddyService;
import nu.yona.server.subscriptions.service.UserAnonymizedDto;
import nu.yona.server.subscriptions.service.UserAnonymizedService;
import nu.yona.server.subscriptions.service.UserDto;
import nu.yona.server.subscriptions.service.UserService;
import nu.yona.server.util.TimeUtil;

@Service
public class ActivityService {
    @Autowired
    private UserService userService;

    @Autowired
    private BuddyService buddyService;

    @Autowired
    private GoalService goalService;

    @Autowired
    private UserAnonymizedService userAnonymizedService;

    @Autowired
    private MessageService messageService;

    @Autowired(required = false)
    private WeekActivityRepository weekActivityRepository;

    @Autowired(required = false)
    private DayActivityRepository dayActivityRepository;

    @Autowired(required = false)
    private MessageRepository messageRepository;

    @Autowired
    private YonaProperties yonaProperties;

    @Autowired
    private AnalysisEngineProxyService analysisEngineProxyService;

    @Transactional
    public Page<WeekActivityOverviewDto> getUserWeekActivityOverviews(UUID userId, Pageable pageable) {
        return executeAndCreateInactivityEntries(
                mia -> getWeekActivityOverviews(userService.getUserAnonymizedId(userId), pageable, mia));
    }

    @Transactional
    public WeekActivityOverviewDto getUserWeekActivityOverview(UUID userId, LocalDate date) {
        return executeAndCreateInactivityEntries(
                mia -> getWeekActivityOverview(userService.getUserAnonymizedId(userId), date, mia));
    }

    @Transactional
    public Page<WeekActivityOverviewDto> getBuddyWeekActivityOverviews(UUID buddyId, Pageable pageable) {
        return executeAndCreateInactivityEntries(
                mia -> getWeekActivityOverviews(getBuddyUserAnonymizedId(buddyId), pageable, mia));
    }

    @Transactional
    public WeekActivityOverviewDto getBuddyWeekActivityOverview(UUID buddyId, LocalDate date) {
        return executeAndCreateInactivityEntries(
                mia -> getWeekActivityOverview(getBuddyUserAnonymizedId(buddyId), date, mia));
    }

    private <T> T executeAndCreateInactivityEntries(Function<Set<IntervalInactivityDto>, T> executor) {
        Set<IntervalInactivityDto> missingInactivities = new HashSet<>();
        // Execute the method that finds missing inactivities
        T retVal = executor.apply(missingInactivities);

        createInactivityEntities(missingInactivities);

        return retVal;
    }

    private void createInactivityEntities(Set<IntervalInactivityDto> missingInactivities) {
        Map<UUID, Set<IntervalInactivityDto>> activitiesByUserAnonymizedId = missingInactivities.stream()
                .collect(Collectors.groupingBy(mia -> mia.getUserAnonymizedId().get(), Collectors.toSet()));

        activitiesByUserAnonymizedId
                .forEach((u, mias) -> analysisEngineProxyService.createInactivityEntities(u, mias));
    }

    private Page<WeekActivityOverviewDto> getWeekActivityOverviews(UUID userAnonymizedId, Pageable pageable,
            Set<IntervalInactivityDto> missingInactivities) {
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);
        Interval interval = getInterval(getCurrentWeekDate(userAnonymized), pageable, ChronoUnit.WEEKS);

        List<WeekActivityOverviewDto> weekActivityOverviews = getWeekActivityOverviews(userAnonymizedId,
                missingInactivities, userAnonymized, interval);
        return new PageImpl<>(weekActivityOverviews, pageable,
                getTotalPageableItems(userAnonymized, ChronoUnit.WEEKS));
    }

    private List<WeekActivityOverviewDto> getWeekActivityOverviews(UUID userAnonymizedId,
            Set<IntervalInactivityDto> missingInactivities, UserAnonymizedDto userAnonymized, Interval interval) {
        Map<LocalDate, Set<WeekActivity>> weekActivityEntitiesByLocalDate = getWeekActivitiesGroupedByDate(
                userAnonymizedId, interval);
        Map<ZonedDateTime, Set<WeekActivity>> weekActivityEntitiesByZonedDate = mapToZonedDateTime(
                weekActivityEntitiesByLocalDate);
        Map<ZonedDateTime, Set<WeekActivityDto>> weekActivityDtosByZonedDate = mapWeekActivitiesToDtos(
                weekActivityEntitiesByZonedDate);
        addMissingInactivity(userAnonymized.getGoals(), weekActivityDtosByZonedDate, interval, ChronoUnit.WEEKS,
                userAnonymized,
                (goal, startOfWeek) -> createAndSaveWeekInactivity(userAnonymized, goal, startOfWeek,
                        LevelOfDetail.WEEK_OVERVIEW, missingInactivities),
                (g, wa) -> createAndSaveInactivityDays(userAnonymized,
                        userAnonymized.getGoalsForActivityCategory(g.getActivityCategory()), wa,
                        missingInactivities));
        return weekActivityDtosByZonedDate.entrySet().stream()
                .sorted((e1, e2) -> e2.getKey().compareTo(e1.getKey()))
                .map(e -> WeekActivityOverviewDto.createInstance(e.getKey(), e.getValue()))
                .collect(Collectors.toList());
    }

    private WeekActivityOverviewDto getWeekActivityOverview(UUID userAnonymizedId, LocalDate date,
            Set<IntervalInactivityDto> missingInactivities) {
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);
        Interval interval = Interval.createWeekInterval(date);

        List<WeekActivityOverviewDto> weekActivityOverviews = getWeekActivityOverviews(userAnonymizedId,
                missingInactivities, userAnonymized, interval);
        return weekActivityOverviews.get(0);
    }

    private Map<ZonedDateTime, Set<WeekActivityDto>> mapWeekActivitiesToDtos(
            Map<ZonedDateTime, Set<WeekActivity>> weekActivityEntitiesByLocalDate) {
        return weekActivityEntitiesByLocalDate.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> mapWeekActivitiesToDtos(e.getValue())));
    }

    private Set<WeekActivityDto> mapWeekActivitiesToDtos(Set<WeekActivity> weekActivityEntities) {
        return weekActivityEntities.stream()
                .map(e -> WeekActivityDto.createInstance(e, LevelOfDetail.WEEK_OVERVIEW))
                .collect(Collectors.toSet());
    }

    private WeekActivityDto createAndSaveWeekInactivity(UserAnonymizedDto userAnonymized, Goal goal,
            ZonedDateTime startOfWeek, LevelOfDetail levelOfDetail,
            Set<IntervalInactivityDto> missingInactivities) {
        return WeekActivityDto.createInstanceInactivity(userAnonymized, goal, startOfWeek, levelOfDetail,
                missingInactivities);
    }

    private void createAndSaveInactivityDays(UserAnonymizedDto userAnonymized, Set<GoalDto> goals,
            WeekActivityDto weekActivity, Set<IntervalInactivityDto> missingInactivities) {
        weekActivity.createRequiredInactivityDays(userAnonymized, goals, LevelOfDetail.WEEK_OVERVIEW,
                missingInactivities);
    }

    private long getTotalPageableItems(Set<BuddyDto> buddies, ChronoUnit timeUnit) {
        return buddies.stream().map(this::getBuddyUserAnonymizedId)
                .map(id -> userAnonymizedService.getUserAnonymized(id))
                .map(ua -> getTotalPageableItems(ua, timeUnit)).max(Long::compareTo).orElse(0L);
    }

    private long getTotalPageableItems(UserAnonymizedDto userAnonymized, ChronoUnit timeUnit) {
        long activityMemoryDays = yonaProperties.getAnalysisService().getActivityMemory().toDays();
        Optional<LocalDateTime> oldestGoalCreationTime = userAnonymized.getOldestGoalCreationTime();
        long activityRecordedDays = oldestGoalCreationTime.isPresent()
                ? (Duration.between(oldestGoalCreationTime.get(), TimeUtil.utcNow()).toDays() + 1)
                : 0;
        long totalDays = Math.min(activityRecordedDays, activityMemoryDays);
        switch (timeUnit) {
        case WEEKS:
            return (long) Math.ceil((double) totalDays / 7);
        case DAYS:
            return totalDays;
        default:
            throw new IllegalArgumentException("timeUnit should be weeks or days");
        }
    }

    private <T extends IntervalActivity> Map<ZonedDateTime, Set<T>> mapToZonedDateTime(
            Map<LocalDate, Set<T>> intervalActivityEntitiesByLocalDate) {
        return intervalActivityEntitiesByLocalDate.entrySet().stream()
                .collect(Collectors.toMap(e -> e.getValue().iterator().next().getStartTime(), Map.Entry::getValue));
    }

    private Map<LocalDate, Set<WeekActivity>> getWeekActivitiesGroupedByDate(UUID userAnonymizedId,
            Interval interval) {
        Set<WeekActivity> weekActivityEntities = weekActivityRepository.findAll(userAnonymizedId,
                interval.startDate, interval.endDate);
        return weekActivityEntities.stream()
                .collect(Collectors.groupingBy(IntervalActivity::getStartDate, Collectors.toSet()));
    }

    @Transactional
    public Page<DayActivityOverviewDto<DayActivityDto>> getUserDayActivityOverviews(UUID userId,
            Pageable pageable) {
        return executeAndCreateInactivityEntries(
                mia -> getDayActivityOverviews(userService.getUserAnonymizedId(userId), pageable, mia));
    }

    @Transactional
    public DayActivityOverviewDto<DayActivityDto> getUserDayActivityOverview(UUID userId, LocalDate date) {
        return executeAndCreateInactivityEntries(
                mia -> getDayActivityOverview(userService.getUserAnonymizedId(userId), date, mia));
    }

    @Transactional
    public Page<DayActivityOverviewDto<DayActivityWithBuddiesDto>> getUserDayActivityOverviewsWithBuddies(
            UUID userId, Pageable pageable) {
        UUID userAnonymizedId = userService.getUserAnonymizedId(userId);
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);

        Interval interval = getInterval(getCurrentDayDate(userAnonymized), pageable, ChronoUnit.DAYS);
        Set<BuddyDto> buddies = buddyService.getBuddiesOfUserThatAcceptedSending(userId);
        List<DayActivityOverviewDto<DayActivityWithBuddiesDto>> dayActivityOverviews = getUserDayActivityOverviewsWithBuddies(
                userAnonymizedId, interval, buddies);
        return new PageImpl<>(dayActivityOverviews, pageable, getTotalPageableItems(buddies, ChronoUnit.DAYS));
    }

    private List<DayActivityOverviewDto<DayActivityWithBuddiesDto>> getUserDayActivityOverviewsWithBuddies(
            UUID userAnonymizedId, Interval interval, Set<BuddyDto> buddies) {
        Set<UUID> userAnonymizedIds = buddies.stream().map(this::getBuddyUserAnonymizedId)
                .collect(Collectors.toSet());
        userAnonymizedIds.add(userAnonymizedId);
        // Goals of the user should only be included in the withBuddies list
        // when at least one buddy has a goal in that category
        Set<UUID> activityCategoryIdsUsedByBuddies = buddies.stream()
                .map(b -> b.getGoals().orElse(Collections.emptySet())).flatMap(Set::stream)
                .map(GoalDto::getActivityCategoryId).collect(Collectors.toSet());

        Map<ZonedDateTime, Set<DayActivityDto>> dayActivityDtosByZonedDate = executeAndCreateInactivityEntries(
                mia -> getDayActivitiesForUserAnonymizedIdsInInterval(userAnonymizedIds,
                        activityCategoryIdsUsedByBuddies, interval, mia));
        return dayActivityEntitiesToOverviewsUserWithBuddies(dayActivityDtosByZonedDate);
    }

    @Transactional
    public DayActivityOverviewDto<DayActivityWithBuddiesDto> getUserDayActivityOverviewWithBuddies(UUID userId,
            LocalDate date) {
        UUID userAnonymizedId = userService.getUserAnonymizedId(userId);
        Interval interval = Interval.createDayInterval(date);
        Set<BuddyDto> buddies = buddyService.getBuddiesOfUserThatAcceptedSending(userId);

        List<DayActivityOverviewDto<DayActivityWithBuddiesDto>> dayActivityOverviews = getUserDayActivityOverviewsWithBuddies(
                userAnonymizedId, interval, buddies);

        return dayActivityOverviews.get(0);
    }

    private Map<ZonedDateTime, Set<DayActivityDto>> getDayActivitiesForUserAnonymizedIdsInInterval(
            Set<UUID> userAnonymizedIds, Set<UUID> relevantActivityCategoryIds, Interval interval,
            Set<IntervalInactivityDto> mia) {
        return userAnonymizedIds.stream()
                .map(id -> getDayActivitiesForCategories(userAnonymizedService.getUserAnonymized(id),
                        relevantActivityCategoryIds, interval, mia))
                .map(Map::entrySet).flatMap(Collection::stream)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> {
                    Set<DayActivityDto> allActivities = new HashSet<>(a);
                    allActivities.addAll(b);
                    return allActivities;
                }));
    }

    private Map<ZonedDateTime, Set<DayActivityDto>> getDayActivitiesForCategories(
            UserAnonymizedDto userAnonymizedDto, Set<UUID> relevantActivityCategoryIds, Interval interval,
            Set<IntervalInactivityDto> mia) {
        Set<GoalDto> relevantGoals = userAnonymizedDto.getGoals().stream()
                .filter(g -> relevantActivityCategoryIds.contains(g.getActivityCategoryId()))
                .collect(Collectors.toSet());
        return getDayActivities(userAnonymizedDto, relevantGoals, interval, mia);
    }

    @Transactional
    public Page<DayActivityOverviewDto<DayActivityDto>> getBuddyDayActivityOverviews(UUID buddyId,
            Pageable pageable) {
        return executeAndCreateInactivityEntries(
                mia -> getDayActivityOverviews(getBuddyUserAnonymizedId(buddyId), pageable, mia));
    }

    @Transactional
    public DayActivityOverviewDto<DayActivityDto> getBuddyDayActivityOverview(UUID buddyId, LocalDate date) {
        return executeAndCreateInactivityEntries(
                mia -> getDayActivityOverview(getBuddyUserAnonymizedId(buddyId), date, mia));
    }

    private UUID getBuddyUserAnonymizedId(UUID buddyId) {
        return getBuddyUserAnonymizedId(buddyService.getBuddy(buddyId));
    }

    private UUID getBuddyUserAnonymizedId(BuddyDto buddy) {
        return buddy.getUserAnonymizedId().orElseThrow(
                () -> new IllegalStateException("Should have user anonymized ID when fetching buddy activity"));
    }

    private Page<DayActivityOverviewDto<DayActivityDto>> getDayActivityOverviews(UUID userAnonymizedId,
            Pageable pageable, Set<IntervalInactivityDto> missingInactivities) {
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);
        Interval interval = getInterval(getCurrentDayDate(userAnonymized), pageable, ChronoUnit.DAYS);

        List<DayActivityOverviewDto<DayActivityDto>> dayActivityOverviews = getDayActivityOverviews(
                missingInactivities, userAnonymized, interval);

        return new PageImpl<>(dayActivityOverviews, pageable,
                getTotalPageableItems(userAnonymized, ChronoUnit.DAYS));
    }

    private DayActivityOverviewDto<DayActivityDto> getDayActivityOverview(UUID userAnonymizedId, LocalDate date,
            Set<IntervalInactivityDto> missingInactivities) {
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);
        Interval interval = Interval.createDayInterval(date);

        List<DayActivityOverviewDto<DayActivityDto>> dayActivityOverviews = getDayActivityOverviews(
                missingInactivities, userAnonymized, interval);

        return dayActivityOverviews.get(0);
    }

    private List<DayActivityOverviewDto<DayActivityDto>> getDayActivityOverviews(
            Set<IntervalInactivityDto> missingInactivities, UserAnonymizedDto userAnonymized, Interval interval) {
        Map<ZonedDateTime, Set<DayActivityDto>> dayActivitiesByZonedDate = getDayActivities(userAnonymized,
                interval, missingInactivities);
        return dayActivityDtosToOverviews(dayActivitiesByZonedDate);
    }

    private Map<ZonedDateTime, Set<DayActivityDto>> getDayActivities(UserAnonymizedDto userAnonymized,
            Interval interval, Set<IntervalInactivityDto> missingInactivities) {
        return getDayActivities(userAnonymized, userAnonymized.getGoals(), interval, missingInactivities);
    }

    private Map<ZonedDateTime, Set<DayActivityDto>> getDayActivities(UserAnonymizedDto userAnonymized,
            Set<GoalDto> relevantGoals, Interval interval, Set<IntervalInactivityDto> missingInactivities) {
        Map<LocalDate, Set<DayActivity>> dayActivityEntitiesByLocalDate = getDayActivitiesGroupedByDate(
                userAnonymized.getId(), relevantGoals, interval);
        Map<ZonedDateTime, Set<DayActivity>> dayActivityEntitiesByZonedDate = mapToZonedDateTime(
                dayActivityEntitiesByLocalDate);
        Map<ZonedDateTime, Set<DayActivityDto>> dayActivityDtosByZonedDate = mapDayActivitiesToDtos(
                dayActivityEntitiesByZonedDate);
        addMissingInactivity(relevantGoals, dayActivityDtosByZonedDate, interval, ChronoUnit.DAYS, userAnonymized,
                (goal, startOfDay) -> createDayInactivity(userAnonymized, goal, startOfDay,
                        LevelOfDetail.DAY_OVERVIEW, missingInactivities),
                (g, a) -> { // Nothing needed here
                });
        return dayActivityDtosByZonedDate;
    }

    private Map<ZonedDateTime, Set<DayActivityDto>> mapDayActivitiesToDtos(
            Map<ZonedDateTime, Set<DayActivity>> dayActivityEntitiesByZonedDate) {
        return dayActivityEntitiesByZonedDate.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> mapDayActivitiesToDtos(e.getValue())));
    }

    private Set<DayActivityDto> mapDayActivitiesToDtos(Set<DayActivity> dayActivityEntities) {
        return dayActivityEntities.stream().map(e -> DayActivityDto.createInstance(e, LevelOfDetail.DAY_OVERVIEW))
                .collect(Collectors.toSet());
    }

    private DayActivityDto createDayInactivity(UserAnonymizedDto userAnonymized, Goal goal,
            ZonedDateTime startOfDay, LevelOfDetail levelOfDetail, Set<IntervalInactivityDto> missingInactivities) {
        return DayActivityDto.createInstanceInactivity(userAnonymized, GoalDto.createInstance(goal), startOfDay,
                levelOfDetail, missingInactivities);
    }

    private List<DayActivityOverviewDto<DayActivityDto>> dayActivityDtosToOverviews(
            Map<ZonedDateTime, Set<DayActivityDto>> dayActivityDtosByZonedDate) {
        return dayActivityDtosByZonedDate.entrySet().stream().sorted((e1, e2) -> e2.getKey().compareTo(e1.getKey()))
                .map(e -> DayActivityOverviewDto.createInstanceForUser(e.getKey(), e.getValue()))
                .collect(Collectors.toList());
    }

    private List<DayActivityOverviewDto<DayActivityWithBuddiesDto>> dayActivityEntitiesToOverviewsUserWithBuddies(
            Map<ZonedDateTime, Set<DayActivityDto>> dayActivityDtosByZonedDate) {
        return dayActivityDtosByZonedDate.entrySet().stream().sorted((e1, e2) -> e2.getKey().compareTo(e1.getKey()))
                .map(e -> DayActivityOverviewDto.createInstanceForUserWithBuddies(e.getKey(), e.getValue()))
                .collect(Collectors.toList());
    }

    private Map<LocalDate, Set<DayActivity>> getDayActivitiesGroupedByDate(UUID userAnonymizedId,
            Set<GoalDto> relevantGoals, Interval interval) {
        List<DayActivity> dayActivityEntities = findAllActivitiesForUserInInterval(userAnonymizedId, relevantGoals,
                interval);
        return dayActivityEntities.stream()
                .collect(Collectors.groupingBy(IntervalActivity::getStartDate, Collectors.toSet()));
    }

    private List<DayActivity> findAllActivitiesForUserInInterval(UUID userAnonymizedId, Set<GoalDto> relevantGoals,
            Interval interval) {
        if (relevantGoals.isEmpty()) {
            // SQL in-query fails when the list is empty, so don't go to the
            // repository with an empty list
            return Collections.emptyList();
        }
        return dayActivityRepository.findAll(userAnonymizedId,
                relevantGoals.stream().map(GoalDto::getGoalId).collect(Collectors.toSet()), interval.startDate,
                interval.endDate);
    }

    private Interval getInterval(LocalDate currentUnitDate, Pageable pageable, ChronoUnit timeUnit) {
        LocalDate startDate = currentUnitDate.minus(pageable.getOffset() + pageable.getPageSize() - 1L, timeUnit);
        LocalDate endDate = currentUnitDate.minus(pageable.getOffset() - 1L, timeUnit);
        return Interval.createInterval(startDate, endDate);
    }

    private LocalDate getCurrentWeekDate(UserAnonymizedDto userAnonymized) {
        LocalDate currentDayDate = getCurrentDayDate(userAnonymized);
        if (currentDayDate.getDayOfWeek() == DayOfWeek.SUNDAY) {
            // take as the first day of week
            return currentDayDate;
        }
        // MONDAY=1, etc.
        return currentDayDate.minusDays(currentDayDate.getDayOfWeek().getValue());
    }

    private LocalDate getCurrentDayDate(UserAnonymizedDto userAnonymized) {
        return LocalDate.now(userAnonymized.getTimeZone());
    }

    private <T extends IntervalActivityDto> void addMissingInactivity(Set<GoalDto> relevantGoals,
            Map<ZonedDateTime, Set<T>> activityEntitiesByDate, Interval interval, ChronoUnit timeUnit,
            UserAnonymizedDto userAnonymized, BiFunction<Goal, ZonedDateTime, T> inactivityEntitySupplier,
            BiConsumer<Goal, T> existingEntityInactivityCompletor) {
        for (LocalDate date = interval.startDate; date.isBefore(interval.endDate); date = date.plus(1, timeUnit)) {
            ZonedDateTime dateAtStartOfInterval = date.atStartOfDay(userAnonymized.getTimeZone());

            Set<GoalDto> activeGoals = getActiveGoals(relevantGoals, dateAtStartOfInterval, timeUnit);
            if (activeGoals.isEmpty()) {
                continue;
            }

            if (!activityEntitiesByDate.containsKey(dateAtStartOfInterval)) {
                activityEntitiesByDate.put(dateAtStartOfInterval, new HashSet<T>());
            }
            Set<T> activityEntitiesAtDate = activityEntitiesByDate.get(dateAtStartOfInterval);
            activeGoals.stream()
                    .map(g -> goalService.getGoalEntityForUserAnonymizedId(userAnonymized.getId(), g.getGoalId()))
                    .forEach(g -> addMissingInactivity(g, dateAtStartOfInterval, activityEntitiesAtDate,
                            inactivityEntitySupplier, existingEntityInactivityCompletor));
        }
    }

    private Set<GoalDto> getActiveGoals(Set<GoalDto> relevantGoals, ZonedDateTime dateAtStartOfInterval,
            ChronoUnit timeUnit) {
        return relevantGoals.stream().filter(g -> g.wasActiveAtInterval(dateAtStartOfInterval, timeUnit))
                .collect(Collectors.toSet());
    }

    private <T extends IntervalActivityDto> void addMissingInactivity(Goal activeGoal,
            ZonedDateTime dateAtStartOfInterval, Set<T> activityEntitiesAtDate,
            BiFunction<Goal, ZonedDateTime, T> inactivityEntitySupplier,
            BiConsumer<Goal, T> existingEntityInactivityCompletor) {
        Optional<T> activityForGoal = getActivityForGoal(activityEntitiesAtDate, activeGoal);
        if (activityForGoal.isPresent()) {
            // even if activity was already recorded, it might be that this is
            // not for the complete period
            // so make the interval activity complete with a consumer
            existingEntityInactivityCompletor.accept(activeGoal, activityForGoal.get());
        } else {
            activityEntitiesAtDate.add(inactivityEntitySupplier.apply(activeGoal, dateAtStartOfInterval));
        }
    }

    private <T extends IntervalActivityDto> Optional<T> getActivityForGoal(Set<T> dayActivityDtosAtDate,
            Goal goal) {
        return dayActivityDtosAtDate.stream().filter(a -> a.getGoalId().equals(goal.getId())).findAny();
    }

    @Transactional
    public WeekActivityDto getUserWeekActivityDetail(UUID userId, LocalDate date, UUID goalId) {
        return executeAndCreateInactivityEntries(
                mia -> getWeekActivityDetail(userId, userService.getUserAnonymizedId(userId), date, goalId, mia));
    }

    @Transactional
    public WeekActivityDto getBuddyWeekActivityDetail(UUID buddyId, LocalDate date, UUID goalId) {
        BuddyDto buddy = buddyService.getBuddy(buddyId);
        return executeAndCreateInactivityEntries(mia -> getWeekActivityDetail(buddy.getUser().getId(),
                getBuddyUserAnonymizedId(buddy), date, goalId, mia));
    }

    private WeekActivityDto getWeekActivityDetail(UUID userId, UUID userAnonymizedId, LocalDate date, UUID goalId,
            Set<IntervalInactivityDto> missingInactivities) {
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);
        WeekActivity weekActivityEntity = weekActivityRepository.findOne(userAnonymizedId, date, goalId);
        if (weekActivityEntity == null) {
            return getMissingInactivity(userId, date, goalId, userAnonymized, ChronoUnit.WEEKS,
                    (goal, startOfWeek) -> createAndSaveWeekInactivity(userAnonymized, goal, startOfWeek,
                            LevelOfDetail.WEEK_DETAIL, missingInactivities));
        }
        WeekActivityDto weekActivityDto = WeekActivityDto.createInstance(weekActivityEntity,
                LevelOfDetail.WEEK_DETAIL);
        weekActivityDto.createRequiredInactivityDays(userAnonymized,
                userAnonymized.getGoalsForActivityCategory(weekActivityEntity.getGoal().getActivityCategory()),
                LevelOfDetail.WEEK_DETAIL, missingInactivities);
        return weekActivityDto;
    }

    @Transactional
    public Page<MessageDto> getUserWeekActivityDetailMessages(UUID userId, LocalDate date, UUID goalId,
            Pageable pageable) {
        Supplier<IntervalActivity> activitySupplier = () -> weekActivityRepository
                .findOne(userService.getUserAnonymizedId(userId), date, goalId);
        return getActivityDetailMessages(userId, activitySupplier, pageable);
    }

    @Transactional
    public Page<MessageDto> getBuddyWeekActivityDetailMessages(UUID userId, UUID buddyId, LocalDate date,
            UUID goalId, Pageable pageable) {
        BuddyDto buddy = buddyService.getBuddy(buddyId);
        Supplier<IntervalActivity> activitySupplier = () -> weekActivityRepository
                .findOne(getBuddyUserAnonymizedId(buddy), date, goalId);
        return getActivityDetailMessages(userId, activitySupplier, pageable);
    }

    @Transactional
    public DayActivityDto getUserDayActivityDetail(UUID userId, LocalDate date, UUID goalId) {
        return executeAndCreateInactivityEntries(
                mia -> getDayActivityDetail(userId, userService.getUserAnonymizedId(userId), date, goalId, mia));
    }

    @Transactional
    public DayActivityDto getBuddyDayActivityDetail(UUID buddyId, LocalDate date, UUID goalId) {
        BuddyDto buddy = buddyService.getBuddy(buddyId);
        return executeAndCreateInactivityEntries(mia -> getDayActivityDetail(buddy.getUser().getId(),
                getBuddyUserAnonymizedId(buddy), date, goalId, mia));
    }

    private DayActivityDto getDayActivityDetail(UUID userId, UUID userAnonymizedId, LocalDate date, UUID goalId,
            Set<IntervalInactivityDto> missingInactivities) {
        UserAnonymizedDto userAnonymized = userAnonymizedService.getUserAnonymized(userAnonymizedId);
        DayActivity dayActivityEntity = dayActivityRepository.findOne(userAnonymizedId, date, goalId);
        if (dayActivityEntity == null) {
            return getMissingInactivity(userId, date, goalId, userAnonymized, ChronoUnit.DAYS,
                    (goal, startOfDay) -> createDayInactivity(userAnonymized, goal, startOfDay,
                            LevelOfDetail.DAY_DETAIL, missingInactivities));
        }
        return DayActivityDto.createInstance(dayActivityEntity, LevelOfDetail.DAY_DETAIL);
    }

    @Transactional
    public Page<MessageDto> getUserDayActivityDetailMessages(UUID userId, LocalDate date, UUID goalId,
            Pageable pageable) {
        Supplier<IntervalActivity> activitySupplier = () -> dayActivityRepository
                .findOne(userService.getUserAnonymizedId(userId), date, goalId);
        return getActivityDetailMessages(userId, activitySupplier, pageable);
    }

    @Transactional
    public Page<MessageDto> getBuddyDayActivityDetailMessages(UUID userId, UUID buddyId, LocalDate date,
            UUID goalId, Pageable pageable) {
        BuddyDto buddy = buddyService.getBuddy(buddyId);
        Supplier<IntervalActivity> activitySupplier = () -> dayActivityRepository
                .findOne(getBuddyUserAnonymizedId(buddy), date, goalId);
        return getActivityDetailMessages(userId, activitySupplier, pageable);
    }

    private Page<MessageDto> getActivityDetailMessages(UUID userId, Supplier<IntervalActivity> activitySupplier,
            Pageable pageable) {
        IntervalActivity dayActivityEntity = activitySupplier.get();
        if (dayActivityEntity == null) {
            return new PageImpl<>(Collections.emptyList());
        }
        return messageService.getActivityRelatedMessages(userId, dayActivityEntity, pageable);
    }

    public List<ActivityDto> getRawActivities(UUID userId, LocalDate date, UUID goalId) {
        return dayActivityRepository.findOne(userService.getUserAnonymizedId(userId), date, goalId).getActivities()
                .stream().map(ActivityDto::createInstance).collect(Collectors.toList());
    }

    @Transactional
    public MessageDto addMessageToDayActivity(UUID userId, UUID buddyId, LocalDate date, UUID goalId,
            PostPutActivityCommentMessageDto message) {
        ActivitySupplier activitySupplier = (b, d, g) -> dayActivityRepository.findOne(getBuddyUserAnonymizedId(b),
                d, g);
        return addMessageToActivity(userId, buddyId, date, goalId, activitySupplier, message);
    }

    @Transactional
    public MessageDto addMessageToWeekActivity(UUID userId, UUID buddyId, LocalDate date, UUID goalId,
            PostPutActivityCommentMessageDto message) {
        ActivitySupplier activitySupplier = (b, d, g) -> weekActivityRepository.findOne(getBuddyUserAnonymizedId(b),
                d, g);
        return addMessageToActivity(userId, buddyId, date, goalId, activitySupplier, message);
    }

    @Transactional
    public MessageDto addMessageToActivity(UUID userId, UUID buddyId, LocalDate date, UUID goalId,
            ActivitySupplier activitySupplier, PostPutActivityCommentMessageDto message) {
        UserDto sendingUser = userService.getPrivateUser(userId);
        BuddyDto buddy = buddyService.getBuddy(buddyId);
        IntervalActivity dayActivityEntity = activitySupplier.get(buddy, date, goalId);
        if (dayActivityEntity == null) {
            throw ActivityServiceException.buddyDayActivityNotFound(userId, buddyId, date, goalId);
        }

        return sendMessagePair(sendingUser, getBuddyUserAnonymizedId(buddy), dayActivityEntity, Optional.empty(),
                Optional.empty(), message.getMessage());
    }

    private MessageDto sendMessagePair(UserDto sendingUser, UUID targetUserAnonymizedId,
            IntervalActivity intervalActivityEntity, Optional<Message> repliedMessageOfSelf,
            Optional<Message> repliedMessageOfBuddy, String message) {
        UUID sendingUserAnonymizedId = sendingUser.getOwnPrivateData().getUserAnonymizedId();
        ActivityCommentMessage messageToBuddy = createMessage(sendingUser, sendingUserAnonymizedId,
                intervalActivityEntity, repliedMessageOfBuddy, false, message);
        ActivityCommentMessage messageToSelf = createMessage(sendingUser, targetUserAnonymizedId,
                intervalActivityEntity, repliedMessageOfSelf, true, message);
        sendMessage(targetUserAnonymizedId, messageToBuddy);
        messageToSelf.setBuddyMessage(messageToBuddy);
        sendMessage(sendingUserAnonymizedId, messageToSelf);

        return messageService.messageToDto(sendingUser, messageToSelf);
    }

    private void sendMessage(UUID targetUserAnonymizedId, ActivityCommentMessage messageEntity) {
        UserAnonymized userAnonymizedEntity = userAnonymizedService.getUserAnonymizedEntity(targetUserAnonymizedId);
        messageService.sendMessage(messageEntity, userAnonymizedEntity.getAnonymousDestination());
        userAnonymizedService.updateUserAnonymized(userAnonymizedEntity);
    }

    private ActivityCommentMessage createMessage(UserDto sendingUser, UUID relatedUserAnonymizedId,
            IntervalActivity intervalActivityEntity, Optional<Message> repliedMessage, boolean isSentItem,
            String messageText) {
        ActivityCommentMessage message;
        BuddyInfoParameters buddyInfoParameters = BuddyMessageDto.createBuddyInfoParametersInstance(sendingUser,
                relatedUserAnonymizedId);
        if (repliedMessage.isPresent()) {
            message = ActivityCommentMessage.createInstance(buddyInfoParameters, intervalActivityEntity, isSentItem,
                    messageText, repliedMessage.get());
        } else {
            message = ActivityCommentMessage.createThreadHeadInstance(buddyInfoParameters, intervalActivityEntity,
                    isSentItem, messageText);
        }
        messageRepository.save(message);
        return message;
    }

    private <T extends IntervalActivityDto> T getMissingInactivity(UUID userId, LocalDate date, UUID goalId,
            UserAnonymizedDto userAnonymized, ChronoUnit timeUnit,
            BiFunction<Goal, ZonedDateTime, T> inactivityEntitySupplier) {
        Goal goal = goalService.getGoalEntityForUserAnonymizedId(userAnonymized.getId(), goalId);
        ZonedDateTime dateAtStartOfInterval = date.atStartOfDay(userAnonymized.getTimeZone());
        if (!goal.wasActiveAtInterval(dateAtStartOfInterval, timeUnit)) {
            throw ActivityServiceException.activityDateGoalMismatch(userId, date, goalId);
        }
        return inactivityEntitySupplier.apply(goal, dateAtStartOfInterval);
    }

    @Transactional
    public MessageDto replyToMessage(UserDto sendingUser, ActivityCommentMessage repliedMessage, String message) {
        UUID targetUserAnonymizedId = repliedMessage.getRelatedUserAnonymizedId().get();
        return sendMessagePair(sendingUser, targetUserAnonymizedId, repliedMessage.getIntervalActivity(),
                Optional.of(repliedMessage), Optional.of(repliedMessage.getSenderCopyMessage()), message);
    }

    @Transactional
    public void deleteAllDayActivityCommentMessages(Goal goal) {
        goal.getWeekActivities().forEach(wa -> messageService
                .deleteMessagesForIntervalActivities(wa.getDayActivities().stream().collect(Collectors.toList())));

        goal.getPreviousVersionOfThisGoal().ifPresent(this::deleteAllDayActivityCommentMessages);
    }

    @Transactional
    public void deleteAllWeekActivityCommentMessages(Goal goal) {
        messageService.deleteMessagesForIntervalActivities(
                goal.getWeekActivities().stream().collect(Collectors.toList()));

        goal.getPreviousVersionOfThisGoal().ifPresent(this::deleteAllWeekActivityCommentMessages);
    }

    @FunctionalInterface
    static interface ActivitySupplier {
        IntervalActivity get(BuddyDto buddy, LocalDate date, UUID goalId);
    }

    /**
     * A time interval represents a period of time between two dates. Intervals are inclusive of the start date and exclusive of
     * the end. The end date is always greater than or equal to the start date. Interval is thread-safe and immutable.
     */
    private static class Interval {
        public final LocalDate startDate;
        public final LocalDate endDate;

        /**
         * Creates an interval that includes the given startDate and excludes the given endDate
         */
        private Interval(LocalDate startDate, LocalDate endDate) {
            assert startDate.isBefore(endDate) || startDate.equals(endDate);

            this.startDate = startDate;
            this.endDate = endDate;
        }

        /**
         * Creates an interval that includes the given startDate and excludes the given endDate
         */
        static Interval createInterval(LocalDate startDate, LocalDate endDate) {
            return new Interval(startDate, endDate);
        }

        /**
         * Creates an interval for just the given day
         */
        static Interval createDayInterval(LocalDate date) {
            return new Interval(date, date.plusDays(1));
        }

        /**
         * Creates an interval that spans a week from the given date
         */
        static Interval createWeekInterval(LocalDate date) {
            return new Interval(date, date.plusWeeks(1));
        }

        @Override
        public String toString() {
            return startDate + " <= d < " + endDate;
        }
    }
}