StatisticsController.java

1
package edu.ucsb.cs156.dining.controllers;
2
3
import edu.ucsb.cs156.dining.repositories.ReviewRepository;
4
import edu.ucsb.cs156.dining.repositories.projections.CommonsRatingProjection;
5
import edu.ucsb.cs156.dining.repositories.projections.CommonsReviewRow;
6
import edu.ucsb.cs156.dining.repositories.projections.ItemRatingProjection;
7
import edu.ucsb.cs156.dining.statuses.ModerationStatus;
8
import edu.ucsb.cs156.dining.util.StatsWindow;
9
import edu.ucsb.cs156.dining.util.TimeBucket;
10
import io.swagger.v3.oas.annotations.Operation;
11
import io.swagger.v3.oas.annotations.tags.Tag;
12
import java.time.LocalDate;
13
import java.time.LocalDateTime;
14
import java.util.ArrayList;
15
import java.util.Comparator;
16
import java.util.HashMap;
17
import java.util.List;
18
import java.util.Map;
19
import lombok.extern.slf4j.Slf4j;
20
import org.springframework.beans.factory.annotation.Autowired;
21
import org.springframework.data.domain.PageRequest;
22
import org.springframework.security.access.prepost.PreAuthorize;
23
import org.springframework.web.bind.annotation.GetMapping;
24
import org.springframework.web.bind.annotation.RequestMapping;
25
import org.springframework.web.bind.annotation.RequestParam;
26
import org.springframework.web.bind.annotation.RestController;
27
28
@Tag(name = "Statistics")
29
@RequestMapping("/api/statistics")
30
@RestController
31
@Slf4j
32
public class StatisticsController extends ApiController {
33
34
  private static final int DEFAULT_LIMIT = 10;
35
  private static final int MAX_LIMIT = 50;
36
  private static final long DEFAULT_MIN_REVIEWS = 3;
37
38
  @Autowired ReviewRepository reviewRepository;
39
40
  public record StatisticsSummary(
41
      long totalApprovedReviews,
42
      long totalMenuItemsReviewed,
43
      long totalCommonsCovered,
44
      LocalDateTime lastUpdated) {}
45
46
  public record RatedItem(
47
      Long itemId,
48
      String name,
49
      String diningCommonsCode,
50
      String mealCode,
51
      Double avgStars,
52
      Long reviewCount) {}
53
54
  public record CommonsAverage(String diningCommonsCode, Double avgStars, Long reviewCount) {}
55
56
  public record CommonsTimeseriesPoint(
57
      String diningCommonsCode, LocalDate bucketStart, Double avgStars, Long reviewCount) {}
58
59
  private record TimeseriesKey(String diningCommonsCode, LocalDate bucketStart) {}
60
61
  @Operation(summary = "Get a summary of review statistics")
62
  @PreAuthorize("hasRole('ROLE_USER')")
63
  @GetMapping(value = "", produces = "application/json")
64
  public StatisticsSummary getSummary() {
65 1 1. getSummary : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::getSummary → KILLED
    return new StatisticsSummary(
66
        reviewRepository.countByStatus(ModerationStatus.APPROVED),
67
        reviewRepository.countDistinctItemsByStatus(ModerationStatus.APPROVED),
68
        reviewRepository.countDistinctCommonsByStatus(ModerationStatus.APPROVED),
69
        reviewRepository.findMaxDateEditedByStatus(ModerationStatus.APPROVED));
70
  }
71
72
  LocalDateTime sinceFor(StatsWindow window) {
73 1 1. sinceFor : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::sinceFor → KILLED
    return window.since(LocalDateTime.now());
74
  }
75
76
  private static int clampLimit(int limit) {
77 1 1. clampLimit : replaced int return with 0 for edu/ucsb/cs156/dining/controllers/StatisticsController::clampLimit → KILLED
    return Math.min(Math.max(limit, 1), MAX_LIMIT);
78
  }
79
80
  private static long clampMinReviews(long minReviews) {
81 1 1. clampMinReviews : replaced long return with 0 for edu/ucsb/cs156/dining/controllers/StatisticsController::clampMinReviews → KILLED
    return Math.max(minReviews, 1L);
82
  }
83
84
  private RatedItem toRatedItem(ItemRatingProjection p) {
85 1 1. toRatedItem : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::toRatedItem → KILLED
    return new RatedItem(
86
        p.getItemId(),
87
        p.getName(),
88
        p.getDiningCommonsCode(),
89
        p.getMealCode(),
90
        p.getAvgStars(),
91
        p.getReviewCount());
92
  }
93
94
  private CommonsAverage toCommonsAverage(CommonsRatingProjection p) {
95 1 1. toCommonsAverage : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::toCommonsAverage → KILLED
    return new CommonsAverage(p.getDiningCommonsCode(), p.getAvgStars(), p.getReviewCount());
96
  }
97
98
  @Operation(summary = "Get best rated menu items by average stars")
99
  @PreAuthorize("hasRole('ROLE_USER')")
100
  @GetMapping(value = "/items/best", produces = "application/json")
101
  public List<RatedItem> bestRatedItems(
102
      @RequestParam(defaultValue = "ALL") StatsWindow window,
103
      @RequestParam(defaultValue = "" + DEFAULT_LIMIT) int limit,
104
      @RequestParam(name = "minReviews", defaultValue = "" + DEFAULT_MIN_REVIEWS) long minReviews) {
105
    int clampedLimit = clampLimit(limit);
106
    long clampedMinReviews = clampMinReviews(minReviews);
107
108
    LocalDateTime since = sinceFor(window);
109
    List<ItemRatingProjection> projections =
110
        reviewRepository.findTopRatedItems(
111
            ModerationStatus.APPROVED, since, clampedMinReviews, PageRequest.of(0, clampedLimit));
112
113 1 1. bestRatedItems : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::bestRatedItems → KILLED
    return projections.stream().map(this::toRatedItem).toList();
114
  }
115
116
  @Operation(summary = "Get worst rated menu items by average stars")
117
  @PreAuthorize("hasRole('ROLE_USER')")
118
  @GetMapping(value = "/items/worst", produces = "application/json")
119
  public List<RatedItem> worstRatedItems(
120
      @RequestParam(defaultValue = "ALL") StatsWindow window,
121
      @RequestParam(defaultValue = "" + DEFAULT_LIMIT) int limit,
122
      @RequestParam(name = "minReviews", defaultValue = "" + DEFAULT_MIN_REVIEWS) long minReviews) {
123
    int clampedLimit = clampLimit(limit);
124
    long clampedMinReviews = clampMinReviews(minReviews);
125
126
    LocalDateTime since = sinceFor(window);
127
    List<ItemRatingProjection> projections =
128
        reviewRepository.findBottomRatedItems(
129
            ModerationStatus.APPROVED, since, clampedMinReviews, PageRequest.of(0, clampedLimit));
130
131 1 1. worstRatedItems : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::worstRatedItems → KILLED
    return projections.stream().map(this::toRatedItem).toList();
132
  }
133
134
  @Operation(summary = "Get average star ratings grouped by dining commons")
135
  @PreAuthorize("hasRole('ROLE_USER')")
136
  @GetMapping(value = "/commons/averages", produces = "application/json")
137
  public List<CommonsAverage> commonsAverages(
138
      @RequestParam(defaultValue = "ALL") StatsWindow window) {
139
    LocalDateTime since = sinceFor(window);
140
    List<CommonsRatingProjection> projections =
141
        reviewRepository.findCommonsAverages(ModerationStatus.APPROVED, since);
142
143 1 1. commonsAverages : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::commonsAverages → KILLED
    return projections.stream().map(this::toCommonsAverage).toList();
144
  }
145
146
  static List<CommonsTimeseriesPoint> computeTimeseries(
147
      List<CommonsReviewRow> rows, TimeBucket bucket) {
148
    Map<TimeseriesKey, List<Integer>> starsByKey = new HashMap<>();
149
150
    for (CommonsReviewRow row : rows) {
151
      LocalDate bucketStart = bucket.floor(row.getDateItemServed());
152
      TimeseriesKey key = new TimeseriesKey(row.getDiningCommonsCode(), bucketStart);
153 1 1. lambda$computeTimeseries$0 : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::lambda$computeTimeseries$0 → KILLED
      starsByKey.computeIfAbsent(key, ignored -> new ArrayList<>()).add(row.getItemsStars());
154
    }
155
156 1 1. computeTimeseries : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::computeTimeseries → KILLED
    return starsByKey.entrySet().stream()
157
        .map(
158
            entry -> {
159
              List<Integer> stars = entry.getValue();
160
              double avgStars = stars.stream().mapToInt(Integer::intValue).average().orElse(0.0);
161 1 1. lambda$computeTimeseries$1 : replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::lambda$computeTimeseries$1 → KILLED
              return new CommonsTimeseriesPoint(
162
                  entry.getKey().diningCommonsCode(),
163
                  entry.getKey().bucketStart(),
164
                  avgStars,
165
                  (long) stars.size());
166
            })
167
        .sorted(
168
            Comparator.comparing(CommonsTimeseriesPoint::diningCommonsCode)
169
                .thenComparing(CommonsTimeseriesPoint::bucketStart))
170
        .toList();
171
  }
172
173
  @Operation(summary = "Get per-commons average ratings over time buckets")
174
  @PreAuthorize("hasRole('ROLE_USER')")
175
  @GetMapping(value = "/commons/timeseries", produces = "application/json")
176
  public List<CommonsTimeseriesPoint> commonsTimeseries(
177
      @RequestParam(defaultValue = "DAY") TimeBucket bucket,
178
      @RequestParam(defaultValue = "ALL") StatsWindow window) {
179
    LocalDateTime since = sinceFor(window);
180
    List<CommonsReviewRow> rows =
181
        reviewRepository.findCommonsReviewRows(ModerationStatus.APPROVED, since);
182
183 1 1. commonsTimeseries : replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::commonsTimeseries → KILLED
    return computeTimeseries(rows, bucket);
184
  }
185
}

Mutations

65

1.1
Location : getSummary
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:api_statistics_null_lastUpdated_serializes_as_null()]
replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::getSummary → KILLED

73

1.1
Location : sinceFor
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:commons_averages_window_SIX_MONTHS_passes_correct_since()]
replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::sinceFor → KILLED

77

1.1
Location : clampLimit
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:items_worst_minReviews_one_passes_through_unchanged()]
replaced int return with 0 for edu/ucsb/cs156/dining/controllers/StatisticsController::clampLimit → KILLED

81

1.1
Location : clampMinReviews
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:items_worst_minReviews_one_passes_through_unchanged()]
replaced long return with 0 for edu/ucsb/cs156/dining/controllers/StatisticsController::clampMinReviews → KILLED

85

1.1
Location : toRatedItem
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:items_best_default_params_uses_ALL_window_limit_10_minReviews_3()]
replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::toRatedItem → KILLED

95

1.1
Location : toCommonsAverage
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:commons_averages_default_window_ALL_returns_expected_shape()]
replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::toCommonsAverage → KILLED

113

1.1
Location : bestRatedItems
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:items_best_default_params_uses_ALL_window_limit_10_minReviews_3()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::bestRatedItems → KILLED

131

1.1
Location : worstRatedItems
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:items_worst_default_params_calls_findBottomRatedItems_and_returns_shape()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::worstRatedItems → KILLED

143

1.1
Location : commonsAverages
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTests]/[method:commons_averages_default_window_ALL_returns_expected_shape()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::commonsAverages → KILLED

153

1.1
Location : lambda$computeTimeseries$0
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerComputeTimeseriesTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerComputeTimeseriesTests]/[method:computeTimeseries_single_row_returns_average_and_count()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::lambda$computeTimeseries$0 → KILLED

156

1.1
Location : computeTimeseries
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerComputeTimeseriesTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerComputeTimeseriesTests]/[method:computeTimeseries_single_row_returns_average_and_count()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::computeTimeseries → KILLED

161

1.1
Location : lambda$computeTimeseries$1
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerComputeTimeseriesTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerComputeTimeseriesTests]/[method:computeTimeseries_single_row_returns_average_and_count()]
replaced return value with null for edu/ucsb/cs156/dining/controllers/StatisticsController::lambda$computeTimeseries$1 → KILLED

183

1.1
Location : commonsTimeseries
Killed by : edu.ucsb.cs156.dining.controllers.StatisticsControllerTimeseriesTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.dining.controllers.StatisticsControllerTimeseriesTests]/[method:commons_timeseries_bucket_WEEK_groups_iso_week()]
replaced return value with Collections.emptyList for edu/ucsb/cs156/dining/controllers/StatisticsController::commonsTimeseries → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0