nu.yona.server.analysis.rest.UserActivityController.java Source code

Java tutorial

Introduction

Here is the source code for nu.yona.server.analysis.rest.UserActivityController.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.rest;

import static nu.yona.server.rest.Constants.PASSWORD_HEADER;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonUnwrapped;

import nu.yona.server.analysis.entities.IntervalActivity;
import nu.yona.server.analysis.service.ActivityCommentMessageDto;
import nu.yona.server.analysis.service.ActivityDto;
import nu.yona.server.analysis.service.DayActivityDto;
import nu.yona.server.analysis.service.DayActivityOverviewDto;
import nu.yona.server.analysis.service.DayActivityWithBuddiesDto;
import nu.yona.server.analysis.service.DayActivityWithBuddiesDto.ActivityForOneUser;
import nu.yona.server.analysis.service.WeekActivityDto;
import nu.yona.server.analysis.service.WeekActivityOverviewDto;
import nu.yona.server.crypto.seckey.CryptoSession;
import nu.yona.server.device.service.UserDeviceDto;
import nu.yona.server.goals.rest.ActivityCategoryController;
import nu.yona.server.goals.rest.GoalController;
import nu.yona.server.messaging.service.MessageDto;
import nu.yona.server.subscriptions.rest.BuddyController;
import nu.yona.server.subscriptions.rest.UserController;
import nu.yona.server.subscriptions.service.GoalIdMapping;

/*
 * Controller to retrieve activity data for a user.
 */
@Controller
@RequestMapping(value = "/users/{userId}/activity", produces = { MediaType.APPLICATION_JSON_VALUE })
public class UserActivityController extends ActivityControllerBase {
    @RequestMapping(value = WEEK_ACTIVITY_OVERVIEWS_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<PagedResources<WeekActivityOverviewResource>> getUserWeekActivityOverviews(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PageableDefault(size = WEEKS_DEFAULT_PAGE_SIZE) Pageable pageable,
            PagedResourcesAssembler<WeekActivityOverviewDto> pagedResourcesAssembler) {
        return getWeekActivityOverviews(password, userId, pagedResourcesAssembler,
                () -> activityService.getUserWeekActivityOverviews(userId, pageable),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = WEEK_ACTIVITY_OVERVIEW_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<WeekActivityOverviewResource> getUserWeekActivityOverview(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr) {
        return getWeekActivityOverview(password, userId, dateStr,
                date -> activityService.getUserWeekActivityOverview(userId, date),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = DAY_OVERVIEWS_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<PagedResources<DayActivityOverviewResource>> getUserDayActivityOverviews(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PageableDefault(size = DAYS_DEFAULT_PAGE_SIZE) Pageable pageable,
            PagedResourcesAssembler<DayActivityOverviewDto<DayActivityDto>> pagedResourcesAssembler) {
        return getDayActivityOverviews(password, userId, pagedResourcesAssembler,
                () -> activityService.getUserDayActivityOverviews(userId, pageable),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = DAY_OVERVIEW_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<DayActivityOverviewResource> getUserDayActivityOverview(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr) {
        return getDayActivityOverview(password, userId, dateStr,
                date -> activityService.getUserDayActivityOverview(userId, date),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = WEEK_ACTIVITY_DETAIL_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<WeekActivityResource> getUserWeekActivityDetail(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr,
            @PathVariable(value = GOAL_PATH_VARIABLE) UUID goalId) {
        return getWeekActivityDetail(password, userId, dateStr,
                date -> activityService.getUserWeekActivityDetail(userId, date, goalId),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = WEEK_ACTIVITY_DETAIL_MESSAGES_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<PagedResources<MessageDto>> getUserWeekActivityDetailMessages(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr,
            @PathVariable(value = GOAL_PATH_VARIABLE) UUID goalId,
            @PageableDefault(size = MESSAGES_DEFAULT_PAGE_SIZE) Pageable pageable,
            PagedResourcesAssembler<MessageDto> pagedResourcesAssembler) {
        return getActivityDetailMessages(password, userId, pagedResourcesAssembler, () -> activityService
                .getUserWeekActivityDetailMessages(userId, WeekActivityDto.parseDate(dateStr), goalId, pageable),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = DAY_ACTIVITY_DETAIL_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<DayActivityResource> getUserDayActivityDetail(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr,
            @PathVariable(value = GOAL_PATH_VARIABLE) UUID goalId) {
        return getDayActivityDetail(password, userId, dateStr,
                date -> activityService.getUserDayActivityDetail(userId, date, goalId),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = DAY_ACTIVITY_DETAIL_MESSAGES_URI_FRAGMENT, method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<PagedResources<MessageDto>> getUserDayActivityDetailMessages(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr,
            @PathVariable(value = GOAL_PATH_VARIABLE) UUID goalId,
            @PageableDefault(size = MESSAGES_DEFAULT_PAGE_SIZE) Pageable pageable,
            PagedResourcesAssembler<MessageDto> pagedResourcesAssembler) {
        return getActivityDetailMessages(password, userId, pagedResourcesAssembler, () -> activityService
                .getUserDayActivityDetailMessages(userId, DayActivityDto.parseDate(dateStr), goalId, pageable),
                new UserActivityLinkProvider(userId));
    }

    @RequestMapping(value = DAY_ACTIVITY_DETAIL_URI_FRAGMENT + "/raw/", method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<ActivitiesResource> getRawActivities(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr,
            @PathVariable(value = GOAL_PATH_VARIABLE) UUID goalId) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            Map<UUID, String> deviceAnonymizedIdToDeviceName = buildDeviceAnonymizedIdToDeviceNameMap(userId);
            return createOkResponse(
                    activityService.getRawActivities(userId, DayActivityDto.parseDate(dateStr), goalId),
                    createRawActivitiesResourceAssembler(userId, deviceAnonymizedIdToDeviceName, dateStr, goalId));
        }
    }

    private Map<UUID, String> buildDeviceAnonymizedIdToDeviceNameMap(UUID userId) {
        return userService.getPrivateUser(userId).getOwnPrivateData().getOwnDevices().stream()
                .collect(Collectors.toMap(UserDeviceDto::getDeviceAnonymizedId, UserDeviceDto::getName));
    }

    /**
     * Get network and app activity of the buddies of this user for the current day, pageable to yesterday and before. The user's
     * own activities are included if any buddy shares the same goal. Note that the name is slightly confusing, as the name
     * suggests that the user activities are supplemented with the buddy activities, while in reality, the buddy activities are
     * supplemented with the user activities. The name is retained because it reflects the external API (URL).
     */
    @RequestMapping(value = "/withBuddies/days/", method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<PagedResources<DayActivityOverviewWithBuddiesResource>> getDayActivityOverviewsWithBuddies(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @RequestParam(value = UserController.REQUESTING_DEVICE_ID_PARAM, required = true) UUID requestingDeviceId,
            @PageableDefault(size = DAYS_DEFAULT_PAGE_SIZE) Pageable pageable,
            PagedResourcesAssembler<DayActivityOverviewDto<DayActivityWithBuddiesDto>> pagedResourcesAssembler) {
        return getDayActivityOverviewsWithBuddies(password, userId, requestingDeviceId, pagedResourcesAssembler,
                () -> activityService.getUserDayActivityOverviewsWithBuddies(userId, pageable));
    }

    private HttpEntity<PagedResources<DayActivityOverviewWithBuddiesResource>> getDayActivityOverviewsWithBuddies(
            Optional<String> password, UUID userId, UUID requestingDeviceId,
            PagedResourcesAssembler<DayActivityOverviewDto<DayActivityWithBuddiesDto>> pagedResourcesAssembler,
            Supplier<Page<DayActivityOverviewDto<DayActivityWithBuddiesDto>>> activitySupplier) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            return getDayActivityOverviewsWithBuddies(userId, requestingDeviceId, pagedResourcesAssembler,
                    activitySupplier);
        }
    }

    private ResponseEntity<PagedResources<DayActivityOverviewWithBuddiesResource>> getDayActivityOverviewsWithBuddies(
            UUID userId, UUID requestingDeviceId,
            PagedResourcesAssembler<DayActivityOverviewDto<DayActivityWithBuddiesDto>> pagedResourcesAssembler,
            Supplier<Page<DayActivityOverviewDto<DayActivityWithBuddiesDto>>> activitySupplier) {
        GoalIdMapping goalIdMapping = GoalIdMapping.createInstance(userService.getPrivateUser(userId));

        return createOkResponse(activitySupplier.get(), pagedResourcesAssembler,
                createResourceAssembler(userId, requestingDeviceId, goalIdMapping));
    }

    private DayActivityOverviewWithBuddiesResourceAssembler createResourceAssembler(UUID userId,
            UUID requestingDeviceId, GoalIdMapping goalIdMapping) {
        return new DayActivityOverviewWithBuddiesResourceAssembler(userId, requestingDeviceId, goalIdMapping);
    }

    private ActivitiesResourceAssembler createRawActivitiesResourceAssembler(UUID userId,
            Map<UUID, String> deviceAnonymizedIdToDeviceName, String dateStr, UUID goalId) {
        return new ActivitiesResourceAssembler(userId, deviceAnonymizedIdToDeviceName, dateStr, goalId);
    }

    /**
     * Get network and app activity of the buddies of this user for the given day. The user's own activities are included if any
     * buddy shares the same goal. Note that the name is slightly confusing, as the name suggests that the user activities are
     * supplemented with the buddy activities, while in reality, the buddy activities are supplemented with the user activities.
     * The name is retained because it reflects the external API (URL).
     */
    @RequestMapping(value = "/withBuddies/days/{date}", method = RequestMethod.GET)
    @ResponseBody
    public HttpEntity<DayActivityOverviewWithBuddiesResource> getDayActivityOverviewWithBuddies(
            @RequestHeader(value = PASSWORD_HEADER) Optional<String> password, @PathVariable UUID userId,
            @RequestParam(value = UserController.REQUESTING_DEVICE_ID_PARAM, required = true) UUID requestingDeviceId,
            @PathVariable(value = DATE_PATH_VARIABLE) String dateStr) {
        return getDayActivityOverviewWithBuddies(password, userId, requestingDeviceId, dateStr,
                date -> activityService.getUserDayActivityOverviewWithBuddies(userId, date));
    }

    private HttpEntity<DayActivityOverviewWithBuddiesResource> getDayActivityOverviewWithBuddies(
            Optional<String> password, UUID userId, UUID requestingDeviceId, String dateStr,
            Function<LocalDate, DayActivityOverviewDto<DayActivityWithBuddiesDto>> activitySupplier) {
        try (CryptoSession cryptoSession = CryptoSession.start(password,
                () -> userService.doPreparationsAndCheckCanAccessPrivateData(userId))) {
            return getDayActivityOverviewWithBuddies(userId, requestingDeviceId, dateStr, activitySupplier);
        }
    }

    private ResponseEntity<DayActivityOverviewWithBuddiesResource> getDayActivityOverviewWithBuddies(UUID userId,
            UUID requestingDeviceId, String dateStr,
            Function<LocalDate, DayActivityOverviewDto<DayActivityWithBuddiesDto>> activitySupplier) {
        LocalDate date = DayActivityDto.parseDate(dateStr);
        GoalIdMapping goalIdMapping = GoalIdMapping.createInstance(userService.getPrivateUser(userId));
        return createOkResponse(activitySupplier.apply(date),
                createResourceAssembler(userId, requestingDeviceId, goalIdMapping));
    }

    @Override
    public void addLinks(GoalIdMapping goalIdMapping, IntervalActivity activity,
            ActivityCommentMessageDto message) {
        LinkProvider linkProvider = new UserActivityLinkProvider(goalIdMapping.getUserId());
        addStandardLinks(goalIdMapping, linkProvider, activity, message);
    }

    public static ControllerLinkBuilder getUserDayActivityOverviewsLinkBuilder(UUID userId) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getUserDayActivityOverviews(null, userId, null, null));
    }

    public static ControllerLinkBuilder getDayActivityOverviewsWithBuddiesLinkBuilder(UUID userId,
            UUID requestingDeviceId) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getDayActivityOverviewsWithBuddies(null, userId, requestingDeviceId, (Pageable) null,
                null));
    }

    public static ControllerLinkBuilder getUserWeekActivityOverviewsLinkBuilder(UUID userId) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getUserWeekActivityOverviews(null, userId, null, null));
    }

    public static ControllerLinkBuilder getUserWeekActivityOverviewLinkBuilder(UUID userId, String dateStr) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getUserWeekActivityOverview(null, userId, dateStr));
    }

    public static ControllerLinkBuilder getUserDayActivityOverviewLinkBuilder(UUID userId, String dateStr) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getUserDayActivityOverview(null, userId, dateStr));
    }

    public static ControllerLinkBuilder getDayActivityOverviewWithBuddiesLinkBuilder(UUID userId,
            UUID requestingDeviceId, String dateStr) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getDayActivityOverviewWithBuddies(null, userId, requestingDeviceId, dateStr));
    }

    public static ControllerLinkBuilder getUserDayActivityDetailLinkBuilder(UUID userId, String dateStr,
            UUID goalId) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getUserDayActivityDetail(null, userId, dateStr, goalId));
    }

    static ControllerLinkBuilder getRawActivitiesLinkBuilder(UUID userId, String dateStr, UUID goalId) {
        UserActivityController methodOn = methodOn(UserActivityController.class);
        return linkTo(methodOn.getRawActivities(null, userId, dateStr, goalId));
    }

    static final class UserActivityLinkProvider implements LinkProvider {
        private final UUID userId;

        public UserActivityLinkProvider(UUID userId) {
            this.userId = userId;
        }

        @Override
        public ControllerLinkBuilder getWeekActivityOverviewLinkBuilder(String dateStr) {
            return UserActivityController.getUserWeekActivityOverviewLinkBuilder(userId, dateStr);
        }

        @Override
        public ControllerLinkBuilder getDayActivityOverviewLinkBuilder(String dateStr) {
            return UserActivityController.getUserDayActivityOverviewLinkBuilder(userId, dateStr);
        }

        @Override
        public ControllerLinkBuilder getDayActivityDetailLinkBuilder(String dateStr, UUID goalId) {
            return UserActivityController.getUserDayActivityDetailLinkBuilder(userId, dateStr, goalId);
        }

        @Override
        public ControllerLinkBuilder getWeekActivityDetailLinkBuilder(String dateStr, UUID goalId) {
            UserActivityController methodOn = methodOn(UserActivityController.class);
            return linkTo(methodOn.getUserWeekActivityDetail(null, userId, dateStr, goalId));
        }

        @Override
        public ControllerLinkBuilder getGoalLinkBuilder(UUID goalId) {
            return GoalController.getGoalLinkBuilder(userId, goalId);
        }

        @Override
        public ControllerLinkBuilder getDayActivityDetailMessagesLinkBuilder(String dateStr, UUID goalId) {
            UserActivityController methodOn = methodOn(UserActivityController.class);
            return linkTo(methodOn.getUserDayActivityDetailMessages(Optional.empty(), userId, dateStr, goalId, null,
                    null));
        }

        @Override
        public Optional<ControllerLinkBuilder> getDayActivityDetailAddCommentLinkBuilder(String dateStr,
                UUID goalId) {
            return Optional.empty();
        }

        @Override
        public ControllerLinkBuilder getWeekActivityDetailMessagesLinkBuilder(String dateStr, UUID goalId) {
            UserActivityController methodOn = methodOn(UserActivityController.class);
            return linkTo(methodOn.getUserWeekActivityDetailMessages(Optional.empty(), userId, dateStr, goalId,
                    null, null));
        }

        @Override
        public Optional<ControllerLinkBuilder> getWeekActivityDetailAddCommentLinkBuilder(String dateStr,
                UUID goalId) {
            return Optional.empty();
        }
    }

    static class DayActivityOverviewWithBuddiesResource
            extends Resource<DayActivityOverviewDto<DayActivityWithBuddiesDto>> {
        private UUID requestingDeviceId;
        private final GoalIdMapping goalIdMapping;

        public DayActivityOverviewWithBuddiesResource(UUID requestingDeviceId, GoalIdMapping goalIdMapping,
                DayActivityOverviewDto<DayActivityWithBuddiesDto> dayActivityOverview) {
            super(dayActivityOverview);
            this.requestingDeviceId = requestingDeviceId;
            this.goalIdMapping = goalIdMapping;
        }

        public List<DayActivityWithBuddiesResource> getDayActivities() {
            return new DayActivityWithBuddiesResourceAssembler(requestingDeviceId, goalIdMapping,
                    getContent().getDateStr()).toResources(getContent().getDayActivities());
        }
    }

    static class DayActivityOverviewWithBuddiesResourceAssembler extends
            ResourceAssemblerSupport<DayActivityOverviewDto<DayActivityWithBuddiesDto>, DayActivityOverviewWithBuddiesResource> {
        private final UUID userId;
        private UUID requestingDeviceId;
        private final GoalIdMapping goalIdMapping;

        public DayActivityOverviewWithBuddiesResourceAssembler(UUID userId, UUID requestingDeviceId,
                GoalIdMapping goalIdMapping) {
            super(ActivityControllerBase.class, DayActivityOverviewWithBuddiesResource.class);
            this.userId = userId;
            this.requestingDeviceId = requestingDeviceId;
            this.goalIdMapping = goalIdMapping;
        }

        @Override
        public DayActivityOverviewWithBuddiesResource toResource(
                DayActivityOverviewDto<DayActivityWithBuddiesDto> dayActivityOverview) {
            DayActivityOverviewWithBuddiesResource resource = instantiateResource(dayActivityOverview);
            addSelfLink(resource);
            return resource;
        }

        @Override
        protected DayActivityOverviewWithBuddiesResource instantiateResource(
                DayActivityOverviewDto<DayActivityWithBuddiesDto> dayActivityOverview) {
            return new DayActivityOverviewWithBuddiesResource(requestingDeviceId, goalIdMapping,
                    dayActivityOverview);
        }

        private void addSelfLink(Resource<DayActivityOverviewDto<DayActivityWithBuddiesDto>> resource) {
            resource.add(UserActivityController.getDayActivityOverviewWithBuddiesLinkBuilder(userId,
                    requestingDeviceId, resource.getContent().getDateStr()).withSelfRel());
        }
    }

    static class DayActivityWithBuddiesResource extends Resource<DayActivityWithBuddiesDto> {
        private UUID requestingDeviceId;
        private final GoalIdMapping goalIdMapping;
        private final String dateStr;

        public DayActivityWithBuddiesResource(UUID requestingDeviceId, GoalIdMapping goalIdMapping, String dateStr,
                DayActivityWithBuddiesDto dayActivity) {
            super(dayActivity);
            this.requestingDeviceId = requestingDeviceId;
            this.goalIdMapping = goalIdMapping;
            this.dateStr = dateStr;
        }

        public List<ActivityForOneUserResource> getDayActivitiesForUsers() {
            return new ActivityForOneUserResourceAssembler(requestingDeviceId, goalIdMapping, dateStr)
                    .toResources(getContent().getDayActivitiesForUsers());
        }
    }

    static class DayActivityWithBuddiesResourceAssembler
            extends ResourceAssemblerSupport<DayActivityWithBuddiesDto, DayActivityWithBuddiesResource> {
        private UUID requestingDeviceId;
        private final GoalIdMapping goalIdMapping;
        private final String dateStr;

        public DayActivityWithBuddiesResourceAssembler(UUID requestingDeviceId, GoalIdMapping goalIdMapping,
                String dateStr) {
            super(ActivityControllerBase.class, DayActivityWithBuddiesResource.class);
            this.requestingDeviceId = requestingDeviceId;
            this.goalIdMapping = goalIdMapping;
            this.dateStr = dateStr;
        }

        @Override
        public DayActivityWithBuddiesResource toResource(DayActivityWithBuddiesDto dayActivity) {
            DayActivityWithBuddiesResource dayActivityResource = instantiateResource(dayActivity);
            addActivityCategoryLink(dayActivityResource);
            return dayActivityResource;
        }

        @Override
        protected DayActivityWithBuddiesResource instantiateResource(DayActivityWithBuddiesDto dayActivity) {
            return new DayActivityWithBuddiesResource(requestingDeviceId, goalIdMapping, dateStr, dayActivity);
        }

        private void addActivityCategoryLink(DayActivityWithBuddiesResource dayActivityResource) {
            dayActivityResource.add(ActivityCategoryController
                    .getActivityCategoryLinkBuilder(dayActivityResource.getContent().getActivityCategoryId())
                    .withRel("activityCategory"));
        }
    }

    static class ActivityForOneUserResource extends Resource<ActivityForOneUser> {
        public ActivityForOneUserResource(ActivityForOneUser dayActivity) {
            super(dayActivity);
        }
    }

    static class ActivityForOneUserResourceAssembler
            extends ResourceAssemblerSupport<ActivityForOneUser, ActivityForOneUserResource> {
        private final GoalIdMapping goalIdMapping;
        private final String dateStr;
        private UUID requestingDeviceId;

        public ActivityForOneUserResourceAssembler(UUID requestingDeviceId, GoalIdMapping goalIdMapping,
                String dateStr) {
            super(ActivityControllerBase.class, ActivityForOneUserResource.class);
            this.requestingDeviceId = requestingDeviceId;
            this.goalIdMapping = goalIdMapping;
            this.dateStr = dateStr;
        }

        @Override
        public ActivityForOneUserResource toResource(ActivityForOneUser dayActivity) {
            ActivityForOneUserResource dayActivityResource = instantiateResource(dayActivity);

            UUID goalId = dayActivity.getGoalId();
            UUID userId = goalIdMapping.getUserId();
            if (goalIdMapping.isUserGoal(goalId)) {
                addGoalLinkForUser(userId, goalId, dayActivityResource);
                addDayDetailsLinkForUser(userId, goalId, dayActivityResource);
                addUserLink(userId, requestingDeviceId, dayActivityResource);
            } else {
                UUID buddyId = goalIdMapping.getBuddyId(goalId);
                addGoalLinkForBuddy(userId, buddyId, goalId, dayActivityResource);
                addDayDetailsLinkForBuddy(userId, buddyId, goalId, dayActivityResource);
                addBuddyLink(userId, buddyId, dayActivityResource);
            }
            return dayActivityResource;
        }

        @Override
        protected ActivityForOneUserResource instantiateResource(ActivityForOneUser dayActivity) {
            return new ActivityForOneUserResource(dayActivity);
        }

        private void addGoalLinkForUser(UUID userId, UUID goalId, ActivityForOneUserResource dayActivityResource) {
            dayActivityResource.add(GoalController.getGoalLinkBuilder(userId, goalId).withRel("goal"));
        }

        private void addDayDetailsLinkForUser(UUID userId, UUID goalId,
                ActivityForOneUserResource dayActivityResource) {
            dayActivityResource.add(UserActivityController
                    .getUserDayActivityDetailLinkBuilder(userId, dateStr, goalId).withRel("dayDetails"));
        }

        private void addUserLink(UUID userId, UUID requestingDeviceId,
                ActivityForOneUserResource dayActivityResource) {
            dayActivityResource
                    .add(UserController.getPrivateUserLink("user", userId, Optional.of(requestingDeviceId)));
        }

        private void addGoalLinkForBuddy(UUID userId, UUID buddyId, UUID goalId,
                ActivityForOneUserResource dayActivityResource) {
            dayActivityResource.add(BuddyController.getGoalLinkBuilder(userId, buddyId, goalId).withRel("goal"));
        }

        private void addDayDetailsLinkForBuddy(UUID userId, UUID buddyId, UUID goalId,
                ActivityForOneUserResource dayActivityResource) {
            dayActivityResource.add(BuddyActivityController
                    .getBuddyDayActivityDetailLinkBuilder(userId, buddyId, dateStr, goalId).withRel("dayDetails"));
        }

        private void addBuddyLink(UUID userId, UUID buddyId, ActivityForOneUserResource dayActivityResource) {
            dayActivityResource
                    .add(BuddyController.getBuddyLinkBuilder(userId, buddyId).withRel(BuddyController.BUDDY_LINK));
        }
    }

    @JsonRootName("activity")
    private static class ActivityWithDeviceDto {
        private final ActivityDto dto;
        private final String deviceName;

        private ActivityWithDeviceDto(ActivityDto dto, String deviceName) {
            this.dto = dto;
            this.deviceName = deviceName;
        }

        @JsonUnwrapped
        public ActivityDto getDto() {
            return dto;
        }

        @SuppressWarnings("unused") // Used as JSon property
        public String getDeviceName() {
            return deviceName;
        }
    }

    public static class ActivitiesResource extends Resources<ActivityWithDeviceDto> {
        public ActivitiesResource(Map<UUID, String> deviceAnonymizedIdToDeviceName,
                List<ActivityDto> rawActivities) {
            super(wrapEnrichActivitiesWithDeviceName(deviceAnonymizedIdToDeviceName, rawActivities));
        }

        private static Iterable<ActivityWithDeviceDto> wrapEnrichActivitiesWithDeviceName(
                Map<UUID, String> deviceAnonymizedIdToDeviceName, List<ActivityDto> rawActivities) {
            return rawActivities.stream()
                    .map(dto -> new ActivityWithDeviceDto(dto, getDeviceName(deviceAnonymizedIdToDeviceName, dto)))
                    .collect(Collectors.toList());
        }

        private static String getDeviceName(Map<UUID, String> deviceAnonymizedIdToDeviceName, ActivityDto dto) {
            return dto.getDeviceAnonymizedId().map(deviceAnonymizedIdToDeviceName::get).orElse("n/a");
        }
    }

    public static class ActivitiesResourceAssembler
            extends ResourceAssemblerSupport<List<ActivityDto>, ActivitiesResource> {
        private final UUID userId;
        private final String dateStr;
        private final UUID goalId;
        private Map<UUID, String> deviceAnonymizedIdToDeviceName;

        public ActivitiesResourceAssembler(UUID userId, Map<UUID, String> deviceAnonymizedIdToDeviceName,
                String dateStr, UUID goalId) {
            super(UserActivityController.class, ActivitiesResource.class);
            this.userId = userId;
            this.deviceAnonymizedIdToDeviceName = deviceAnonymizedIdToDeviceName;
            this.dateStr = dateStr;
            this.goalId = goalId;
        }

        @Override
        public ActivitiesResource toResource(List<ActivityDto> rawActivities) {
            ActivitiesResource resource = instantiateResource(rawActivities);
            addSelfLink(resource);
            return resource;
        }

        @Override
        protected ActivitiesResource instantiateResource(List<ActivityDto> rawActivities) {
            return new ActivitiesResource(deviceAnonymizedIdToDeviceName, rawActivities);
        }

        private void addSelfLink(ActivitiesResource resource) {
            resource.add(getRawActivitiesLinkBuilder(userId, dateStr, goalId).withSelfRel());
        }
    }
}