-
Notifications
You must be signed in to change notification settings - Fork 1
Viktors dolzenko task/2 #51
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
Open
ViktorsDolzenko
wants to merge
5
commits into
viktors-dolzenko
Choose a base branch
from
viktors-dolzenko--task/2
base: viktors-dolzenko
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
50 changes: 49 additions & 1 deletion
50
src/main/java/lv/ctco/springboottemplate/exception/GlobalExceptionHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,64 @@ | ||
| package lv.ctco.springboottemplate.exception; | ||
|
|
||
| import jakarta.validation.ConstraintViolationException; | ||
| import org.springframework.context.MessageSource; | ||
| import org.springframework.context.i18n.LocaleContextHolder; | ||
| import org.springframework.context.support.DefaultMessageSourceResolvable; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.validation.BindException; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
| import org.springframework.web.server.ResponseStatusException; | ||
|
|
||
| @RestControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| private final MessageSource messageSource; | ||
|
|
||
| public GlobalExceptionHandler(MessageSource messageSource) { | ||
| this.messageSource = messageSource; | ||
| } | ||
|
|
||
| @ExceptionHandler(ResponseStatusException.class) | ||
| public ResponseEntity<String> handleResponseStatusException(ResponseStatusException ex) { | ||
| HttpStatus status = HttpStatus.resolve(ex.getStatusCode().value()); | ||
| HttpStatus effectiveStatus = status != null ? status : HttpStatus.INTERNAL_SERVER_ERROR; | ||
| String reason = ex.getReason() != null ? ex.getReason() : message("errors.request.failed"); | ||
|
|
||
| return ResponseEntity.status(effectiveStatus).body(reason); | ||
| } | ||
|
|
||
| @ExceptionHandler(BindException.class) | ||
| public ResponseEntity<String> handleBindException(BindException ex) { | ||
| String message = | ||
| ex.getBindingResult().getAllErrors().stream() | ||
| .findFirst() | ||
| .map(DefaultMessageSourceResolvable::getDefaultMessage) | ||
| .orElse(message("errors.invalid.request.parameters")); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); | ||
| } | ||
|
|
||
| @ExceptionHandler(ConstraintViolationException.class) | ||
| public ResponseEntity<String> handleConstraintViolationException( | ||
| ConstraintViolationException ex) { | ||
| String message = | ||
| ex.getConstraintViolations().stream() | ||
| .findFirst() | ||
| .map(violation -> violation.getMessage()) | ||
| .orElse(message("errors.invalid.request.parameters")); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<String> handleException(Exception ex) { | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | ||
| .body("Oops, something went wrong, try later"); | ||
| .body(message("errors.unexpected")); | ||
| } | ||
|
|
||
| private String message(String code, Object... args) { | ||
| return messageSource.getMessage(code, args, LocaleContextHolder.getLocale()); | ||
| } | ||
| } |
41 changes: 41 additions & 0 deletions
41
src/main/java/lv/ctco/springboottemplate/features/statistics/StatisticsController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package lv.ctco.springboottemplate.features.statistics; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.Valid; | ||
| import java.time.LocalDate; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsDetailedDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsRequestDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsSummaryDto; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.ModelAttribute; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/statistics") | ||
| @Tag(name = "Statistics Controller", description = "Todo statistics endpoints") | ||
| public class StatisticsController { | ||
|
|
||
| private final StatisticsService statisticsService; | ||
|
|
||
| public StatisticsController(StatisticsService statisticsService) { | ||
| this.statisticsService = statisticsService; | ||
| } | ||
|
|
||
| @GetMapping | ||
| @Operation(summary = "Get todo statistics") | ||
| public ResponseEntity<?> getStatistics(@Valid @ModelAttribute StatisticsRequestDto input) { | ||
| LocalDate fromDate = input.fromDate(); | ||
| LocalDate toDate = input.toDate(); | ||
|
|
||
| if ("summary".equalsIgnoreCase(input.normalizedFormat())) { | ||
| StatisticsSummaryDto summary = statisticsService.getSummary(fromDate, toDate); | ||
| return ResponseEntity.ok(summary); | ||
| } | ||
|
|
||
| StatisticsDetailedDto detailed = statisticsService.getDetailed(fromDate, toDate); | ||
| return ResponseEntity.ok(detailed); | ||
| } | ||
| } | ||
84 changes: 84 additions & 0 deletions
84
src/main/java/lv/ctco/springboottemplate/features/statistics/StatisticsMapperService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package lv.ctco.springboottemplate.features.statistics; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsDetailedDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsSummaryDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsTodoItemDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsTodosDto; | ||
| import lv.ctco.springboottemplate.features.todo.Todo; | ||
| import org.bson.Document; | ||
| import org.springframework.data.mongodb.core.aggregation.AggregationResults; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| public class StatisticsMapperService { | ||
|
|
||
| public StatisticsSummaryDto toSummary(AggregationResults<Document> results) { | ||
| long total = 0L; | ||
| long completed = 0L; | ||
| Map<String, Long> userStats = new HashMap<>(); | ||
|
|
||
| for (Document doc : results) { | ||
| AggregatedRow row = toAggregatedRow(doc); | ||
|
|
||
| total += row.count(); | ||
| if (row.completed()) { | ||
| completed += row.count(); | ||
| } | ||
| userStats.merge(row.createdBy(), row.count(), Long::sum); | ||
| } | ||
|
|
||
| long pending = total - completed; | ||
| return new StatisticsSummaryDto(total, completed, pending, userStats); | ||
| } | ||
|
|
||
| public StatisticsDetailedDto toDetailed( | ||
| StatisticsSummaryDto summary, StatisticsTodosDto todosDto) { | ||
| return new StatisticsDetailedDto( | ||
| summary.totalTodos(), | ||
| summary.completedTodos(), | ||
| summary.pendingTodos(), | ||
| summary.userStats(), | ||
| todosDto); | ||
| } | ||
|
|
||
| public StatisticsTodosDto toTodosDto(List<Todo> todos) { | ||
| List<StatisticsTodoItemDto> completedTodos = | ||
| todos.stream() | ||
| .filter(Todo::completed) | ||
| .map( | ||
| todo -> | ||
| new StatisticsTodoItemDto( | ||
| todo.id(), | ||
| todo.title(), | ||
| todo.createdBy(), | ||
| todo.createdAt(), | ||
| todo.updatedAt())) | ||
| .toList(); | ||
|
|
||
| List<StatisticsTodoItemDto> pendingTodos = | ||
| todos.stream() | ||
| .filter(todo -> !todo.completed()) | ||
| .map( | ||
| todo -> | ||
| new StatisticsTodoItemDto( | ||
| todo.id(), todo.title(), todo.createdBy(), todo.createdAt(), null)) | ||
| .toList(); | ||
|
|
||
| return new StatisticsTodosDto(completedTodos, pendingTodos); | ||
| } | ||
|
|
||
| private AggregatedRow toAggregatedRow(Document doc) { | ||
| Document id = (Document) doc.get("_id"); | ||
| String createdBy = id.getString("createdBy"); | ||
| boolean isCompleted = Boolean.TRUE.equals(id.getBoolean("completed")); | ||
| Number countNumber = doc.get("count", Number.class); | ||
| long count = countNumber != null ? countNumber.longValue() : 0L; | ||
|
|
||
| return new AggregatedRow(createdBy, isCompleted, count); | ||
| } | ||
|
|
||
| private record AggregatedRow(String createdBy, boolean completed, long count) {} | ||
| } |
98 changes: 98 additions & 0 deletions
98
src/main/java/lv/ctco/springboottemplate/features/statistics/StatisticsService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package lv.ctco.springboottemplate.features.statistics; | ||
|
|
||
| import java.time.Instant; | ||
| import java.time.LocalDate; | ||
| import java.time.ZoneOffset; | ||
| import java.util.List; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsDetailedDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsSummaryDto; | ||
| import lv.ctco.springboottemplate.features.statistics.models.StatisticsTodosDto; | ||
| import lv.ctco.springboottemplate.features.todo.Todo; | ||
| import lv.ctco.springboottemplate.features.todo.TodoService; | ||
| import org.bson.Document; | ||
| import org.springframework.data.mongodb.core.MongoTemplate; | ||
| import org.springframework.data.mongodb.core.aggregation.Aggregation; | ||
| import org.springframework.data.mongodb.core.aggregation.AggregationResults; | ||
| import org.springframework.data.mongodb.core.aggregation.GroupOperation; | ||
| import org.springframework.data.mongodb.core.aggregation.MatchOperation; | ||
| import org.springframework.data.mongodb.core.query.Criteria; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| public class StatisticsService { | ||
|
|
||
| private final TodoService todoService; | ||
| private final MongoTemplate mongoTemplate; | ||
| private final StatisticsMapperService statisticsMapperService; | ||
|
|
||
| public StatisticsService( | ||
| TodoService todoService, | ||
| MongoTemplate mongoTemplate, | ||
| StatisticsMapperService statisticsMapperService) { | ||
| this.todoService = todoService; | ||
| this.mongoTemplate = mongoTemplate; | ||
| this.statisticsMapperService = statisticsMapperService; | ||
| } | ||
|
|
||
| public StatisticsSummaryDto getSummary(LocalDate from, LocalDate to) { | ||
| return aggregateSummary(from, to); | ||
| } | ||
|
|
||
| public StatisticsDetailedDto getDetailed(LocalDate from, LocalDate to) { | ||
| StatisticsSummaryDto summary = aggregateSummary(from, to); | ||
| List<Todo> todos = findTodosInRange(from, to); | ||
| StatisticsTodosDto todosDto = statisticsMapperService.toTodosDto(todos); | ||
| return statisticsMapperService.toDetailed(summary, todosDto); | ||
| } | ||
|
|
||
| private StatisticsSummaryDto aggregateSummary(LocalDate from, LocalDate to) { | ||
| Aggregation aggregation = buildAggregation(from, to); | ||
| AggregationResults<Document> results = | ||
| mongoTemplate.aggregate(aggregation, "todos", Document.class); | ||
| return statisticsMapperService.toSummary(results); | ||
| } | ||
|
|
||
| private Aggregation buildAggregation(LocalDate from, LocalDate to) { | ||
| Criteria dateCriteria = buildDateCriteria(from, to); | ||
|
|
||
| GroupOperation groupByUserAndCompletion = | ||
| Aggregation.group("createdBy", "completed").count().as("count"); | ||
|
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. Usually we define such string as constants. May be even in dedicated file if there are many related constants |
||
|
|
||
| if (dateCriteria != null) { | ||
| MatchOperation match = Aggregation.match(dateCriteria); | ||
| return Aggregation.newAggregation(match, groupByUserAndCompletion); | ||
| } | ||
|
|
||
| return Aggregation.newAggregation(groupByUserAndCompletion); | ||
| } | ||
|
|
||
| private List<Todo> findTodosInRange(LocalDate from, LocalDate to) { | ||
| Instant fromInstant = from != null ? atStartOfDay(from) : null; | ||
| Instant toInstant = to != null ? atEndOfDay(to) : null; | ||
|
|
||
| return todoService.getTodosByCreatedAtRange(fromInstant, toInstant); | ||
| } | ||
|
|
||
| private Criteria buildDateCriteria(LocalDate from, LocalDate to) { | ||
| Instant fromInstant = from != null ? atStartOfDay(from) : null; | ||
| Instant toInstant = to != null ? atEndOfDay(to) : null; | ||
|
|
||
| if (fromInstant != null && toInstant != null) { | ||
| return Criteria.where("createdAt").gte(fromInstant).lte(toInstant); | ||
| } else if (fromInstant != null) { | ||
| return Criteria.where("createdAt").gte(fromInstant); | ||
| } else if (toInstant != null) { | ||
| return Criteria.where("createdAt").lte(toInstant); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private Instant atStartOfDay(LocalDate date) { | ||
| return date.atStartOfDay().toInstant(ZoneOffset.UTC); | ||
| } | ||
|
|
||
| private Instant atEndOfDay(LocalDate date) { | ||
| return date.atTime(23, 59, 59).toInstant(ZoneOffset.UTC); | ||
| } | ||
| } | ||
10 changes: 10 additions & 0 deletions
10
...ain/java/lv/ctco/springboottemplate/features/statistics/models/StatisticsDetailedDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.models; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| public record StatisticsDetailedDto( | ||
| long totalTodos, | ||
| long completedTodos, | ||
| long pendingTodos, | ||
| Map<String, Long> userStats, | ||
| StatisticsTodosDto todos) {} |
28 changes: 28 additions & 0 deletions
28
...main/java/lv/ctco/springboottemplate/features/statistics/models/StatisticsRequestDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.models; | ||
|
|
||
| import jakarta.validation.constraints.Pattern; | ||
| import java.time.LocalDate; | ||
| import lv.ctco.springboottemplate.validation.statistics.ValidStatisticsRequest; | ||
|
|
||
| @ValidStatisticsRequest | ||
| public record StatisticsRequestDto( | ||
| String from, | ||
| String to, | ||
| @Pattern( | ||
| regexp = "^(summary|detailed)$", | ||
| flags = Pattern.Flag.CASE_INSENSITIVE, | ||
| message = "{statistics.format.invalid}") | ||
| String format) { | ||
|
|
||
| public LocalDate fromDate() { | ||
| return from != null && !from.isBlank() ? LocalDate.parse(from) : null; | ||
| } | ||
|
|
||
| public LocalDate toDate() { | ||
| return to != null && !to.isBlank() ? LocalDate.parse(to) : null; | ||
| } | ||
|
|
||
| public String normalizedFormat() { | ||
| return format == null ? "summary" : format; | ||
| } | ||
| } |
6 changes: 6 additions & 0 deletions
6
...main/java/lv/ctco/springboottemplate/features/statistics/models/StatisticsSummaryDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.models; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| public record StatisticsSummaryDto( | ||
| long totalTodos, long completedTodos, long pendingTodos, Map<String, Long> userStats) {} |
6 changes: 6 additions & 0 deletions
6
...ain/java/lv/ctco/springboottemplate/features/statistics/models/StatisticsTodoItemDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.models; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| public record StatisticsTodoItemDto( | ||
| String id, String title, String createdBy, Instant createdAt, Instant completedAt) {} |
6 changes: 6 additions & 0 deletions
6
src/main/java/lv/ctco/springboottemplate/features/statistics/models/StatisticsTodosDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package lv.ctco.springboottemplate.features.statistics.models; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record StatisticsTodosDto( | ||
| List<StatisticsTodoItemDto> completed, List<StatisticsTodoItemDto> pending) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to refactor this field to enum