Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package lv.ctco.springboottemplate.errorhandling;

public final class ErrorMessages {
public static final String TODO_STATS_DATE_RANGE_INVALID =
"Either from, to, or both can be provided. 'from' date must be before or equal to 'to' date";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package lv.ctco.springboottemplate.errorhandling;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this can be import shortened

MethodArgumentNotValidException.class,
BindException.class
})
public ResponseEntity<Map<String, Object>> handleValidationExceptions(Exception ex) {
List<String> errors = new ArrayList<>();

if (ex instanceof MethodArgumentNotValidException invalidArgumentException) {
errors =
invalidArgumentException.getBindingResult().getAllErrors().stream()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like error extraction is the same, why then this IF is required?

.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
} else if (ex instanceof BindException be) {
errors =
be.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
}

return ResponseEntity.badRequest()
.body(Map.of("status", 400, "error", "Bad Request", "messages", errors));
}

@org.springframework.web.bind.annotation.ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, String>> handleServerError(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Internal server error", "details", ex.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ public record Todo(
String createdBy,
String updatedBy,
Instant createdAt,
Instant updatedAt) {}
Instant updatedAt,
Instant completedAt) {}
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,106 @@ CommandLineRunner initDatabase(TodoRepository todoRepository) {
var todos =
List.of(
new Todo(
null, "Buy groceries", "Milk, eggs, bread", false, "system", "system", now, now),
null,
"Buy groceries",
"Milk, eggs, bread",
false,
"user3",
"user3",
now,
null,
null),
new Todo(
null,
"Call dentist",
"Schedule annual checkup",
true,
"user1",
"system",
"system",
now,
now,
now),
new Todo(
null,
"Fix bug in production",
"High priority issue #123",
false,
"system",
"system",
"user1",
"user3",
now,
now),
null,
null),
new Todo(
null,
"Write documentation",
"Update API docs",
false,
"system",
"system",
"user2",
"user4",
now,
now),
null,
null),
new Todo(
null,
"Plan vacation",
"Research destinations",
true,
"user2",
"user5",
now,
now,
now),
// 👇 new 5 todos
new Todo(
null,
"Clean workspace",
"Organize desk and archive old files",
false,
"user2",
"user1",
now,
null,
null),
new Todo(
null,
"Submit expense report",
"Expenses for August",
true,
"user3",
"system",
"system",
now,
now));
now,
now),
new Todo(
null,
"Prepare presentation",
"Slides for Monday’s meeting",
false,
"user3",
"user2",
now,
null,
null),
new Todo(
null,
"Refactor authentication module",
"Improve security and code quality",
true,
"user1",
"user2",
now,
now,
now),
new Todo(
null,
"Read new tech article",
"Microservices best practices",
false,
"user2",
"user3",
now,
null,
null));

todoRepository.saveAll(todos);
log.info("Initialized database with {} todo items", todos.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,17 @@ public List<Todo> searchTodos(String title) {

public Todo createTodo(String title, String description, boolean completed, String createdBy) {
var now = Instant.now();
var todo = new Todo(null, title, description, completed, createdBy, createdBy, now, now);
var todo =
new Todo(
null,
title,
description,
completed,
createdBy,
createdBy,
now,
now,
completed ? Instant.now() : null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? now : null

return todoRepository.save(todo);
}

Expand All @@ -69,7 +79,8 @@ public Optional<Todo> updateTodo(
existingTodo.createdBy(),
updatedBy,
existingTodo.createdAt(),
Instant.now());
Instant.now(),
completed ? Instant.now() : existingTodo.completedAt());
return todoRepository.save(updatedTodo);
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package lv.ctco.springboottemplate.features.todo_statistics;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lv.ctco.springboottemplate.features.todo.Todo;
import lv.ctco.springboottemplate.features.todo_statistics.dto.TodoStatistics;
import lv.ctco.springboottemplate.features.todo_statistics.dto.TodoStatisticsRequest;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

/** REST controller for getting statistics for {@link Todo} items. */
@RestController
@RequestMapping("/api/statistics")
@Tag(name = "Statistics Controller", description = "Provides statistics for Todo items")
public class TodoStatisticsController {
private final TodoStatisticsService statisticsService;

public TodoStatisticsController(TodoStatisticsService statisticsService) {
this.statisticsService = statisticsService;
}

@GetMapping
@Operation(summary = "Get todo items statistics")
public ResponseEntity<TodoStatistics> getStatistics(
@Valid @ParameterObject @ModelAttribute TodoStatisticsRequest request) {

TodoStatistics stats = statisticsService.makeStatistics(request);

return ResponseEntity.ok(stats);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package lv.ctco.springboottemplate.features.todo_statistics;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

public enum TodoStatisticsFormatEnum {
SUMMARY("summary"),
DETAILED("detailed");

private final String value;

TodoStatisticsFormatEnum(String value) {
this.value = value;
}

@JsonValue
public String getValue() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(y) great

return value;
}

@JsonCreator
public static TodoStatisticsFormatEnum fromString(String value) {
for (TodoStatisticsFormatEnum format : TodoStatisticsFormatEnum.values()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to convert to Java Stream API

if (format.value.equalsIgnoreCase(value)) {
return format;
}
}
throw new IllegalArgumentException(
"Unknown format: " + value + "must be summary, detailed or empty");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lv.ctco.springboottemplate.features.todo_statistics;

import com.mongodb.lang.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
public class TodoStatisticsFormatEnumConverter
implements Converter<String, TodoStatisticsFormatEnum> {
@Override
public TodoStatisticsFormatEnum convert(@Nullable String source) {
if (source == null || source.isBlank()) {
return TodoStatisticsFormatEnum.SUMMARY;
}
return TodoStatisticsFormatEnum.fromString(source);
}
}
Loading