-
Notifications
You must be signed in to change notification settings - Fork 1
Warlockxins task 2 #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: warlockxins
Are you sure you want to change the base?
Changes from all commits
93fd9c2
b96aca9
ff24726
0467a77
876d924
14c5f0c
038750a
4122fdc
9bfb579
312204d
66d034a
1017449
fca44a0
3ef23a7
831c496
9e57c09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package lv.ctco.springboottemplate.features.statistics; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import java.time.Instant; | ||
| import java.time.LocalDate; | ||
| import java.time.ZoneOffset; | ||
| import lv.ctco.springboottemplate.features.statistics.dto.ResponseFormat; | ||
| import org.springframework.format.annotation.DateTimeFormat; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.lang.Nullable; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/statistics") | ||
| @Tag(name = "Todo Controller", description = "Todo management endpoints") | ||
| public class StatisticsController { | ||
| private final StatisticsService statisticsService; | ||
|
|
||
| public StatisticsController(StatisticsService statisticsService) { | ||
| this.statisticsService = statisticsService; | ||
| } | ||
|
|
||
| @GetMapping | ||
| @Operation(summary = "Get todo statistics") | ||
| public ResponseEntity<Object> getStatistics( | ||
| @Parameter(description = "From date in ISO format (yyyy-MM-dd)") | ||
| @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) | ||
| @Nullable | ||
| @RequestParam(name = "from", required = false) | ||
| LocalDate from, | ||
| @Parameter(description = "To date in ISO format (yyyy-MM-dd)") | ||
| @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) | ||
| @Nullable | ||
| @RequestParam(name = "to", required = false) | ||
| LocalDate to, | ||
| @Parameter( | ||
| description = | ||
| """ | ||
| Response format. **Allowed values**:<br> | ||
| - `summary`: returns overview information.<br> | ||
| - `detailed`: returns overview information and separate complete and pending Todos. | ||
| """, | ||
| example = "summary") | ||
| @RequestParam(name = "format") | ||
| String formatString) { | ||
|
|
||
| ResponseFormat format; | ||
| try { | ||
| format = ResponseFormat.valueOf(formatString.toUpperCase()); | ||
| } catch (IllegalArgumentException e) { | ||
| return ResponseEntity.badRequest().body("Method parameter 'format': Failed to convert"); | ||
| } | ||
|
|
||
| if (from == null && to == null) { | ||
| return ResponseEntity.badRequest().body("Either 'from' or 'to' should be provided"); | ||
| } | ||
|
|
||
| Instant fromInstant = from == null ? null : from.atStartOfDay(ZoneOffset.UTC).toInstant(); | ||
| Instant toInstant = to == null ? null : to.atStartOfDay(ZoneOffset.UTC).toInstant(); | ||
|
|
||
| return ResponseEntity.ok(statisticsService.getStatistics(format, fromInstant, toInstant)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package lv.ctco.springboottemplate.features.statistics; | ||
|
|
||
| import java.time.Instant; | ||
| import lv.ctco.springboottemplate.features.statistics.dto.ResponseFormat; | ||
| import lv.ctco.springboottemplate.features.statistics.dto.TodoStatsDto; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| public class StatisticsService { | ||
| private final TodoStatsRepository todoStatsRepository; | ||
|
|
||
| public StatisticsService(TodoStatsRepository todoRepository) { | ||
| this.todoStatsRepository = todoRepository; | ||
| } | ||
|
|
||
| public TodoStatsDto getStatistics(ResponseFormat format, Instant from, Instant to) { | ||
| return todoStatsRepository.getStats(format, from, to); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| package lv.ctco.springboottemplate.features.statistics; | ||
|
|
||
| import java.time.Instant; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import lv.ctco.springboottemplate.features.statistics.dto.*; | ||
| import org.bson.Document; | ||
| import org.springframework.data.mongodb.core.MongoTemplate; | ||
| import org.springframework.data.mongodb.core.aggregation.*; | ||
| import org.springframework.data.mongodb.core.query.Criteria; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
BagumEpmak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public class TodoStatsRepository { | ||
|
|
||
| private final MongoTemplate mongoTemplate; | ||
|
|
||
| public TodoStatsRepository(MongoTemplate mongoTemplate) { | ||
| this.mongoTemplate = mongoTemplate; | ||
| } | ||
|
|
||
| Aggregation getSummaryStatsAggregation(ResponseFormat format, Instant from, Instant to) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it should be a private method |
||
| Criteria criteria = new Criteria(); | ||
|
|
||
| if (from != null && to != null) { | ||
| criteria = Criteria.where("createdAt").gte(from).lte(to); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to have string constans or enums for field names |
||
| criteria.lte(to); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lte is called twice |
||
| } else if (from != null) { | ||
| criteria = Criteria.where("createdAt").gte(from); | ||
| } else if (to != null) { | ||
| criteria = Criteria.where("createdAt").lte(to); | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like lines 23-32 should be:Criteria criteria = new Criteria(); |
||
| MatchOperation matchStage = Aggregation.match(criteria); | ||
|
|
||
| // --- First facet: counts --- | ||
| FacetOperation countsFacet = | ||
| Aggregation.facet( | ||
| Aggregation.group() | ||
| .count() | ||
| .as("totalTodos") | ||
| .sum( | ||
| ConditionalOperators.when(Criteria.where("completed").is(true)) | ||
| .then(1) | ||
| .otherwise(0)) | ||
| .as("completedTodos") | ||
| .sum( | ||
| ConditionalOperators.when(Criteria.where("completed").is(false)) | ||
| .then(1) | ||
| .otherwise(0)) | ||
| .as("pendingTodos")) | ||
| .as("counts") | ||
|
|
||
| // --- Second facet: userStats --- | ||
| .and(Aggregation.group("createdBy").count().as("count")) | ||
| .as("userStats"); | ||
|
|
||
| if (format == ResponseFormat.DETAILED) { | ||
| // --- Third facet: completed todos | ||
| countsFacet = | ||
| countsFacet | ||
| .and( | ||
| Aggregation.match(Criteria.where("completed").is(true)), | ||
| Aggregation.project("id", "title", "createdBy", "createdAt", "updatedAt") | ||
| .and("updatedAt") | ||
| .as("completedAt")) | ||
| .as("completedTodos") | ||
|
|
||
| // --- Facet 4: pending todos --- | ||
| .and( | ||
| Aggregation.match(Criteria.where("completed").is(false)), | ||
| Aggregation.project("id", "title", "createdBy", "createdAt")) | ||
| .as("pendingTodos"); | ||
| } | ||
|
|
||
| // --- Final project stage --- | ||
| ProjectionOperation project = | ||
| Aggregation.project() | ||
| .and(ArrayOperators.ArrayElemAt.arrayOf("counts.totalTodos").elementAt(0)) | ||
| .as("totalTodos") | ||
| .and(ArrayOperators.ArrayElemAt.arrayOf("counts.completedTodos").elementAt(0)) | ||
| .as("completedTodos") | ||
| .and(ArrayOperators.ArrayElemAt.arrayOf("counts.pendingTodos").elementAt(0)) | ||
| .as("pendingTodos") | ||
| .and("userStats") | ||
| .as("userStats"); | ||
|
|
||
| if (format == ResponseFormat.DETAILED) { | ||
| project = | ||
| project | ||
| .and(ConditionalOperators.ifNull("completedTodos").then(List.of())) | ||
| .as("todos.completed") | ||
| .and(ConditionalOperators.ifNull("pendingTodos").then(List.of())) | ||
| .as("todos.pending"); | ||
| } | ||
|
|
||
| // Build the full aggregation | ||
| return Aggregation.newAggregation(matchStage, countsFacet, project); | ||
| } | ||
|
|
||
| Long extractLong(Document doc, String key) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be private |
||
| Number n = doc.get(key, Number.class); | ||
| if (n == null) { | ||
| return 0L; | ||
| } | ||
| return n.longValue(); | ||
| } | ||
|
|
||
| public TodoStatsDto getStats(ResponseFormat format, Instant from, Instant to) { | ||
| Aggregation aggregation = getSummaryStatsAggregation(format, from, to); | ||
|
|
||
| Document doc = | ||
| mongoTemplate.aggregate(aggregation, "todos", Document.class).getUniqueMappedResult(); | ||
|
|
||
| if (doc == null) { | ||
| return new TodoStatsDto(0, 0, 0, Map.of(), null); | ||
| } | ||
|
|
||
| List<Document> userStatsList = (List<Document>) doc.get("userStats"); | ||
| Map<String, Long> userStats = | ||
| userStatsList.stream() | ||
| .collect( | ||
| java.util.stream.Collectors.toMap( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do you need package name (java.util.stream) as a prefix? |
||
| d -> d.getString("_id"), d -> ((Number) d.get("count")).longValue())); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extractLong? |
||
|
|
||
| TodoDetailsDto details = null; | ||
|
|
||
| if (format == ResponseFormat.DETAILED) { | ||
| List<CompletedTodoDto> completedTodos = | ||
| ((List<Document>) ((Document) doc.get("todos")).get("completed")) | ||
| .stream() | ||
| .map( | ||
| d -> | ||
| new CompletedTodoDto( | ||
| d.getString("id"), | ||
| d.getString("title"), | ||
| d.getString("createdBy"), | ||
| d.getDate("createdAt").toInstant(), | ||
| d.getDate("completedAt").toInstant())) | ||
| .toList(); | ||
|
|
||
| List<PendingTodoDto> pendingTodos = | ||
| ((List<Document>) ((Document) doc.get("todos")).get("pending")) | ||
| .stream() | ||
| .map( | ||
| d -> | ||
| new PendingTodoDto( | ||
| d.getString("id"), | ||
| d.getString("title"), | ||
| d.getString("createdBy"), | ||
| d.getDate("createdAt").toInstant())) | ||
| .toList(); | ||
|
|
||
| details = new TodoDetailsDto(completedTodos, pendingTodos); | ||
| } | ||
|
|
||
| return new TodoStatsDto( | ||
| extractLong(doc, "totalTodos"), | ||
| extractLong(doc, "completedTodos"), | ||
| extractLong(doc, "pendingTodos"), | ||
| userStats, | ||
| details); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.dto; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| public record CompletedTodoDto( | ||
| String id, String title, String createdBy, Instant createdAt, Instant completedAt) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.dto; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| public record PendingTodoDto(String id, String title, String createdBy, Instant createdAt) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.dto; | ||
|
|
||
| public enum ResponseFormat { | ||
| SUMMARY("summary"), | ||
| DETAILED("detailed"); | ||
|
|
||
| private final String value; | ||
|
|
||
| ResponseFormat(String format) { | ||
| this.value = format; | ||
| } | ||
|
|
||
| public String getValue() { | ||
| return value; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.dto; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record TodoDetailsDto(List<CompletedTodoDto> completed, List<PendingTodoDto> pending) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.dto; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
| import java.util.Map; | ||
|
|
||
| @JsonInclude(JsonInclude.Include.NON_NULL) | ||
| public record TodoStatsDto( | ||
| long totalTodos, | ||
| long completedTodos, | ||
| long pendingTodos, | ||
| Map<String, Long> userStats, | ||
| TodoDetailsDto todos) {} |
Uh oh!
There was an error while loading. Please reload this page.