diff --git a/pom.xml b/pom.xml index 2db888c..ac238ab 100644 --- a/pom.xml +++ b/pom.xml @@ -51,11 +51,13 @@ h2 test + org.springframework.boot spring-boot-starter-test test + org.springframework.boot spring-boot-starter-validation diff --git a/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java b/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java new file mode 100644 index 0000000..c453ae0 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class DuplicateDataException extends RuntimeException { + public DuplicateDataException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java b/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java new file mode 100644 index 0000000..214dfa4 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java @@ -0,0 +1,7 @@ +package ru.practicum.shareit.exception; + +public class ForbiddenAccessException extends RuntimeException { + public ForbiddenAccessException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/NotFoundException.java b/src/main/java/ru/practicum/shareit/exception/NotFoundException.java new file mode 100644 index 0000000..9dcff00 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/NotFoundException.java @@ -0,0 +1,13 @@ +package ru.practicum.shareit.exception; + +import java.util.function.Supplier; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public static Supplier supplier(String message, Object... args) { + return () -> new NotFoundException(message.formatted(args)); + } +} diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java b/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java new file mode 100644 index 0000000..4e2e596 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.exception.dto; + +public record ErrorResponse(String name, String message) { +} diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java b/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java new file mode 100644 index 0000000..497330f --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java @@ -0,0 +1,6 @@ +package ru.practicum.shareit.exception.dto; + +import java.util.List; + +public record ValidationErrorResponse(List violations) { +} diff --git a/src/main/java/ru/practicum/shareit/exception/dto/Violation.java b/src/main/java/ru/practicum/shareit/exception/dto/Violation.java new file mode 100644 index 0000000..e01906e --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/dto/Violation.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.exception.dto; + +public record Violation(String fieldName, String message) { +} diff --git a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..8e95630 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,81 @@ +package ru.practicum.shareit.exception.handler; + +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.practicum.shareit.exception.DuplicateDataException; +import ru.practicum.shareit.exception.ForbiddenAccessException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.exception.dto.ErrorResponse; +import ru.practicum.shareit.exception.dto.ValidationErrorResponse; +import ru.practicum.shareit.exception.dto.Violation; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse onException(Exception ex) { + log.error("Error occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("internal server error", + "An error occurred while processing request"); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ValidationErrorResponse onConstraintValidationException( + ConstraintViolationException ex + ) { + final List violations = ex.getConstraintViolations().stream() + .map( + violation -> new Violation( + violation.getPropertyPath().toString(), + violation.getMessage() + ) + ) + .collect(Collectors.toList()); + log.warn(violations.toString()); + return new ValidationErrorResponse(violations); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ValidationErrorResponse onMethodArgumentNotValidException( + MethodArgumentNotValidException ex + ) { + final List violations = ex.getBindingResult().getFieldErrors().stream() + .map(error -> new Violation(error.getField(), error.getDefaultMessage())) + .collect(Collectors.toList()); + log.warn(violations.toString()); + return new ValidationErrorResponse(violations); + } + + @ExceptionHandler(NotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse onNotFoundException(NotFoundException ex) { + log.warn("Not found exception occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("not found", ex.getMessage()); + } + + @ExceptionHandler(DuplicateDataException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ErrorResponse onDuplicateDataException(DuplicateDataException ex) { + log.warn("Duplicate data exception occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("duplicate data", ex.getMessage()); + } + + @ExceptionHandler(ForbiddenAccessException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ErrorResponse onForbiddenAccessException(ForbiddenAccessException ex) { + log.warn("Forbidden access exception occurred while processing request {}", ex.getMessage()); + return new ErrorResponse("forbidden", ex.getMessage()); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java index bb17668..36ff3be 100644 --- a/src/main/java/ru/practicum/shareit/item/ItemController.java +++ b/src/main/java/ru/practicum/shareit/item/ItemController.java @@ -1,12 +1,61 @@ package ru.practicum.shareit.item; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.NewItemDto; +import ru.practicum.shareit.item.dto.UpdateItemDto; -/** - * TODO Sprint add-controllers. - */ +import java.util.Collection; + +@Slf4j +@Validated @RestController @RequestMapping("/items") +@RequiredArgsConstructor public class ItemController { + private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id"; + private final ItemService itemService; + + @GetMapping("/{itemId}") + public ItemDto getItem( + @RequestHeader(SHARER_USER_ID_HEADER) long userId, + @PathVariable long itemId + ) { + return itemService.getItemOfUserById(userId, itemId); + } + + @GetMapping + public Collection getItems( + @RequestHeader(SHARER_USER_ID_HEADER) long userId + ) { + return itemService.getAllItemsOfUser(userId); + } + + @PostMapping + public ItemDto createItem( + @RequestHeader(SHARER_USER_ID_HEADER) long userId, + @RequestBody @Valid NewItemDto newItemDto + ) { + return itemService.saveItem(userId, newItemDto); + } + + @PatchMapping("/{itemId}") + public ItemDto updateItem( + @RequestHeader(SHARER_USER_ID_HEADER) long userId, + @PathVariable long itemId, + @RequestBody @Valid UpdateItemDto updatedItem + ) { + return itemService.updateItem(userId, itemId, updatedItem); + } + + @GetMapping("/search") + public Collection searchItems( + @RequestParam(name = "text") String query + ) { + return itemService.searchItems(query); + } } diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java new file mode 100644 index 0000000..93d3883 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java @@ -0,0 +1,45 @@ +package ru.practicum.shareit.item; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.NewItemDto; +import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.model.Item; + +@UtilityClass +public class ItemMapper { + + public ItemDto toItemDto(Item item) { + return new ItemDto( + item.getId(), + item.getName(), + item.getDescription(), + item.isAvailable(), + item.getRequest() != null ? item.getRequest().getId() : null + ); + } + + public Item toItem(NewItemDto newItemDto) { + Item item = new Item(); + item.setName(newItemDto.name()); + item.setDescription(newItemDto.description()); + item.setAvailable(newItemDto.available()); + return item; + } + + public Item updateItem(Item item, UpdateItemDto updateItemDto) { + if (updateItemDto.hasName()) { + item.setName(updateItemDto.name()); + } + + if (updateItemDto.hasDescription()) { + item.setDescription(updateItemDto.description()); + } + + if (updateItemDto.hasAvailable()) { + item.setAvailable(updateItemDto.available()); + } + + return item; + } +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java new file mode 100644 index 0000000..86db07f --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemService.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.item; + +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.NewItemDto; +import ru.practicum.shareit.item.dto.UpdateItemDto; + +import java.util.Collection; + +public interface ItemService { + ItemDto getItemOfUserById(long userId, long itemId); + + Collection getAllItemsOfUser(long userId); + + ItemDto saveItem(long userId, NewItemDto newItem); + + ItemDto updateItem(long userId, long itemId, UpdateItemDto updatedItem); + + Collection searchItems(String query); +} diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java new file mode 100644 index 0000000..13609f2 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java @@ -0,0 +1,90 @@ +package ru.practicum.shareit.item; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exception.ForbiddenAccessException; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.item.dto.ItemDto; +import ru.practicum.shareit.item.dto.NewItemDto; +import ru.practicum.shareit.item.dto.UpdateItemDto; +import ru.practicum.shareit.item.model.Item; +import ru.practicum.shareit.item.repository.ItemRepository; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.util.Collection; +import java.util.Collections; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ItemServiceImpl implements ItemService { + private final ItemRepository itemRepository; + private final UserRepository userRepository; + + @Override + public ItemDto getItemOfUserById(long userId, long itemId) { + getUserOrThrow(userId); + + return itemRepository.findById(itemId) + .map(ItemMapper::toItemDto) + .orElseThrow(NotFoundException.supplier("item with id %d not found", itemId)); + } + + @Override + public Collection getAllItemsOfUser(long userId) { + getUserOrThrow(userId); + + return itemRepository.findAllByUserId(userId) + .stream() + .map(ItemMapper::toItemDto) + .toList(); + } + + @Override + public ItemDto saveItem(long userId, NewItemDto newItem) { + User owner = getUserOrThrow(userId); + Item item = ItemMapper.toItem(newItem); + item.setOwner(owner); + item = itemRepository.save(item); + return ItemMapper.toItemDto(item); + } + + @Override + public ItemDto updateItem(long userId, long itemId, UpdateItemDto newItem) { + getUserOrThrow(userId); + Item item = getItemOrThrow(itemId); + if (item.getOwner().getId() != userId) { + throw new ForbiddenAccessException("Only owner of item can update it"); + } + Item updatedItem = ItemMapper.updateItem(item, newItem); + itemRepository.save(updatedItem); + return ItemMapper.toItemDto(updatedItem); + } + + + @Override + public Collection searchItems(String query) { + if (query.isBlank()) { + return Collections.emptyList(); + } + + return itemRepository.searchItems(query) + .stream() + .map(ItemMapper::toItemDto) + .toList(); + } + + private Item getItemOrThrow(long itemId) { + return itemRepository.findById(itemId).orElseThrow( + NotFoundException.supplier("Item with id %d not found", itemId) + ); + } + + private User getUserOrThrow(long userId) { + return userRepository.findById(userId).orElseThrow( + NotFoundException.supplier("User with id %d not found", userId) + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java index 9319d7d..30863dd 100644 --- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java +++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java @@ -1,7 +1,10 @@ package ru.practicum.shareit.item.dto; -/** - * TODO Sprint add-controllers. - */ -public class ItemDto { +public record ItemDto( + Long id, + String name, + String description, + boolean available, + Long requestId +) { } diff --git a/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java new file mode 100644 index 0000000..65648c3 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.item.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record NewItemDto( + @NotBlank String name, + @NotBlank String description, + @NotNull Boolean available +) { +} diff --git a/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java new file mode 100644 index 0000000..8332e19 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.item.dto; + +public record UpdateItemDto( + String name, + String description, + Boolean available +) { + public boolean hasName() { + return name != null && !name.isBlank(); + } + + public boolean hasDescription() { + return description != null && !description.isBlank(); + } + + public boolean hasAvailable() { + return available != null; + } +} diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java index 44eb73d..775a4cd 100644 --- a/src/main/java/ru/practicum/shareit/item/model/Item.java +++ b/src/main/java/ru/practicum/shareit/item/model/Item.java @@ -1,7 +1,23 @@ package ru.practicum.shareit.item.model; -/** - * TODO Sprint add-controllers. - */ +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; +import ru.practicum.shareit.request.model.ItemRequest; +import ru.practicum.shareit.user.model.User; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor public class Item { + Long id; + String name; + String description; + boolean available; + User owner; + ItemRequest request; + } diff --git a/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java new file mode 100644 index 0000000..a74f783 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java @@ -0,0 +1,56 @@ +package ru.practicum.shareit.item.repository; + +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.item.model.Item; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Repository +public class InMemoryItemRepository implements ItemRepository { + private final Map items = new HashMap<>(); + + @Override + public Optional findById(long id) { + return Optional.ofNullable(items.get(id)); + } + + @Override + public Collection findAllByUserId(long userId) { + return items.values() + .stream() + .filter(item -> item.getOwner().getId() == userId) + .toList(); + } + + @Override + public Item save(Item item) { + Long id = generateNextId(); + item.setId(id); + + items.put(id, item); + + return item; + } + + @Override + public Collection searchItems(String query) { + String lowercaseQuery = query.trim().toLowerCase(); + + return items.values() + .stream() + .filter(Item::isAvailable) + .filter(item -> item.getName().toLowerCase().contains(lowercaseQuery) + || item.getDescription().toLowerCase().contains(lowercaseQuery)) + .toList(); + } + + private Long generateNextId() { + Long nextId = items.keySet().stream() + .max(Long::compareTo) + .orElse(0L); + return ++nextId; + } +} diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java new file mode 100644 index 0000000..6426fb6 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java @@ -0,0 +1,16 @@ +package ru.practicum.shareit.item.repository; + +import ru.practicum.shareit.item.model.Item; + +import java.util.Collection; +import java.util.Optional; + +public interface ItemRepository { + Optional findById(long id); + + Collection findAllByUserId(long userId); + + Item save(Item item); + + Collection searchItems(String query); +} diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java deleted file mode 100644 index 95d6f23..0000000 --- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.request; - -/** - * TODO Sprint add-item-requests. - */ -public class ItemRequest { -} diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java new file mode 100644 index 0000000..6f4ab59 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java @@ -0,0 +1,14 @@ +package ru.practicum.shareit.request.model; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +/** + * TODO Sprint add-item-requests. + */ +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ItemRequest { + Long id; +} diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java deleted file mode 100644 index ae6e7f3..0000000 --- a/src/main/java/ru/practicum/shareit/user/User.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.practicum.shareit.user; - -/** - * TODO Sprint add-controllers. - */ -public class User { -} diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java index 03039b9..8516884 100644 --- a/src/main/java/ru/practicum/shareit/user/UserController.java +++ b/src/main/java/ru/practicum/shareit/user/UserController.java @@ -1,12 +1,58 @@ package ru.practicum.shareit.user; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; -/** - * TODO Sprint add-controllers. - */ +import java.util.Collection; + +@Slf4j +@Validated @RestController -@RequestMapping(path = "/users") +@RequestMapping("/users") +@RequiredArgsConstructor public class UserController { + private final UserService userService; + + @GetMapping + public Collection getAllUsers() { + log.trace("get all users requested"); + return userService.findAll(); + } + + @GetMapping("/{userId}") + public UserDto getUser(@PathVariable long userId) { + log.trace("get user requested with id: {}", userId); + return userService.findById(userId); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UserDto createUser(@RequestBody @Valid NewUserDto newUserDto) { + log.trace("create user requested with body: {}", newUserDto); + return userService.save(newUserDto); + } + + @PatchMapping("/{userId}") + public UserDto updateUser( + @RequestBody @Valid UpdateUserDto updateUserDto, + @PathVariable long userId + ) { + updateUserDto.setId(userId); + log.trace("update user requested with id: {} and body {}", userId, updateUserDto); + return userService.update(updateUserDto); + } + + @DeleteMapping("/{userId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable long userId) { + log.trace("delete user requested with id: {}", userId); + userService.delete(userId); + } } diff --git a/src/main/java/ru/practicum/shareit/user/UserMapper.java b/src/main/java/ru/practicum/shareit/user/UserMapper.java new file mode 100644 index 0000000..f94c933 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserMapper.java @@ -0,0 +1,37 @@ +package ru.practicum.shareit.user; + +import lombok.experimental.UtilityClass; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; + +@UtilityClass +public class UserMapper { + + public UserDto toUserDto(User user) { + return new UserDto( + user.getId(), + user.getName(), + user.getEmail()); + } + + public User toUser(NewUserDto newUser) { + User user = new User(); + user.setName(newUser.name()); + user.setEmail(newUser.email()); + return user; + } + + public User updateUser(User user, UpdateUserDto updatedUser) { + if (updatedUser.hasName()) { + user.setName(updatedUser.getName()); + } + + if (updatedUser.hasEmail()) { + user.setEmail(updatedUser.getEmail()); + } + + return user; + } +} diff --git a/src/main/java/ru/practicum/shareit/user/UserService.java b/src/main/java/ru/practicum/shareit/user/UserService.java new file mode 100644 index 0000000..3bdf54a --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserService.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.user; + +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; + +import java.util.Collection; + +public interface UserService { + Collection findAll(); + + UserDto findById(long id); + + UserDto save(NewUserDto user); + + UserDto update(UpdateUserDto user); + + void delete(long id); +} diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java new file mode 100644 index 0000000..bb48c34 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java @@ -0,0 +1,60 @@ +package ru.practicum.shareit.user; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.shareit.exception.NotFoundException; +import ru.practicum.shareit.user.dto.NewUserDto; +import ru.practicum.shareit.user.dto.UpdateUserDto; +import ru.practicum.shareit.user.dto.UserDto; +import ru.practicum.shareit.user.model.User; +import ru.practicum.shareit.user.repository.UserRepository; + +import java.util.Collection; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + + @Override + public Collection findAll() { + return userRepository.findAll() + .stream() + .map(UserMapper::toUserDto) + .toList(); + } + + @Override + public UserDto findById(long id) { + return UserMapper.toUserDto(getUserOrThrow(id)); + } + + @Override + public UserDto save(NewUserDto newUser) { + User user = UserMapper.toUser(newUser); + user = userRepository.save(user); + return UserMapper.toUserDto(user); + } + + @Override + public UserDto update(UpdateUserDto newUser) { + User user = getUserOrThrow(newUser.getId()); + User updatedUser = UserMapper.updateUser(user, newUser); + userRepository.update(updatedUser); + return UserMapper.toUserDto(updatedUser); + } + + @Override + public void delete(long id) { + getUserOrThrow(id); + userRepository.delete(id); + } + + private User getUserOrThrow(long id) { + return userRepository.findById(id).orElseThrow( + NotFoundException.supplier("User with id %d not found", id) + ); + } +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java b/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java new file mode 100644 index 0000000..3351658 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java @@ -0,0 +1,11 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record NewUserDto( + @NotBlank String name, + @NotNull @Email String email +) { +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java new file mode 100644 index 0000000..5bbd748 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java @@ -0,0 +1,26 @@ +package ru.practicum.shareit.user.dto; + +import jakarta.validation.constraints.Email; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class UpdateUserDto { + Long id; + String name; + + @Email + String email; + + public boolean hasName() { + return name != null && !name.isBlank(); + } + + public boolean hasEmail() { + return email != null; + } +} diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java new file mode 100644 index 0000000..96feaeb --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java @@ -0,0 +1,4 @@ +package ru.practicum.shareit.user.dto; + +public record UserDto(Long id, String name, String email) { +} diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java new file mode 100644 index 0000000..7cd8314 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/model/User.java @@ -0,0 +1,17 @@ +package ru.practicum.shareit.user.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@NoArgsConstructor +@AllArgsConstructor +public class User { + Long id; + String name; + String email; +} diff --git a/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java new file mode 100644 index 0000000..7d7bd18 --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java @@ -0,0 +1,74 @@ +package ru.practicum.shareit.user.repository; + +import org.springframework.stereotype.Repository; +import ru.practicum.shareit.exception.DuplicateDataException; +import ru.practicum.shareit.user.model.User; + +import java.util.*; + +@Repository +public class InMemoryUserRepository implements UserRepository { + private final Map users = new HashMap<>(); + private final Set userEmails = new HashSet<>(); + + @Override + public Collection findAll() { + return Collections.unmodifiableCollection(users.values()); + } + + @Override + public Optional findById(long id) { + User user = users.get(id); + if (user == null) { + return Optional.empty(); + } + + return Optional.of(new User( + user.getId(), + user.getName(), + user.getEmail())); + } + + @Override + public User save(User user) { + Long id = generateNextId(); + user.setId(id); + + if (userEmails.contains(user.getEmail())) { + throw new DuplicateDataException("email %s already exists".formatted(user.getEmail())); + } + + users.put(id, user); + userEmails.add(user.getEmail()); + return user; + } + + @Override + public void update(User user) { + User currentUser = users.get(user.getId()); + + if (!currentUser.getEmail().equals(user.getEmail())) { + if (userEmails.contains(user.getEmail())) { + throw new DuplicateDataException("email %s already exists".formatted(user.getEmail())); + } + + userEmails.remove(currentUser.getEmail()); + userEmails.add(user.getEmail()); + } + + users.put(user.getId(), user); + } + + @Override + public void delete(long id) { + User removedUser = users.remove(id); + userEmails.remove(removedUser.getEmail()); + } + + private Long generateNextId() { + Long nextId = users.keySet().stream() + .max(Long::compareTo) + .orElse(0L); + return ++nextId; + } +} diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java new file mode 100644 index 0000000..be812dc --- /dev/null +++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java @@ -0,0 +1,19 @@ +package ru.practicum.shareit.user.repository; + +import ru.practicum.shareit.user.model.User; + +import java.util.Collection; +import java.util.Optional; + +public interface UserRepository { + + Collection findAll(); + + Optional findById(long id); + + User save(User user); + + void update(User user); + + void delete(long id); +}