From 989befed71c7900ed1f46202b8f1ec8fc5e6d676 Mon Sep 17 00:00:00 2001 From: "mykola.filimonov" Date: Sun, 22 Oct 2023 14:58:34 +0300 Subject: [PATCH 1/2] HT-14 implementation of Pure Rest API - Uniform interface used springframework.hateoas and links added to all endpoints at least every implement self link, and usually on list of all elements of the same type; - Client-server, we implement only server side, we do not care about client side, so we can use browser, Postman, and any what we want; - Layered we have simple app without additional layers between client and server - Stateless all services and controllers not save any data, and can extract data only by id directly from repo - Cachable used ShallowEtagHeaderFilter as auto implemented Etag handler, and this bullet require more learning from my side --- .../configuration/ETagConfiguration.java | 14 +++ .../controller/RestExceptionHandler.java | 14 +++ .../controller/UserController.java | 89 +++++++++++++++++++ .../controller/assembler/NoteAssembler.java | 23 +++++ .../controller/assembler/UserAssembler.java | 24 +++++ .../com/example/purerestapi/entity/Note.java | 13 +++ .../com/example/purerestapi/entity/User.java | 17 ++++ .../exception/UserNotFoundException.java | 8 ++ .../repository/UserRepository.java | 36 ++++++++ .../purerestapi/service/UserService.java | 48 ++++++++++ 10 files changed, 286 insertions(+) create mode 100644 src/main/java/com/example/purerestapi/configuration/ETagConfiguration.java create mode 100644 src/main/java/com/example/purerestapi/controller/RestExceptionHandler.java create mode 100644 src/main/java/com/example/purerestapi/controller/UserController.java create mode 100644 src/main/java/com/example/purerestapi/controller/assembler/NoteAssembler.java create mode 100644 src/main/java/com/example/purerestapi/controller/assembler/UserAssembler.java create mode 100644 src/main/java/com/example/purerestapi/entity/Note.java create mode 100644 src/main/java/com/example/purerestapi/entity/User.java create mode 100644 src/main/java/com/example/purerestapi/exception/UserNotFoundException.java create mode 100644 src/main/java/com/example/purerestapi/repository/UserRepository.java create mode 100644 src/main/java/com/example/purerestapi/service/UserService.java diff --git a/src/main/java/com/example/purerestapi/configuration/ETagConfiguration.java b/src/main/java/com/example/purerestapi/configuration/ETagConfiguration.java new file mode 100644 index 0000000..92f51cf --- /dev/null +++ b/src/main/java/com/example/purerestapi/configuration/ETagConfiguration.java @@ -0,0 +1,14 @@ +package com.example.purerestapi.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +@Configuration +public class ETagConfiguration { + + @Bean + public ShallowEtagHeaderFilter shallowEtagHeaderFilter() { + return new ShallowEtagHeaderFilter(); + } +} diff --git a/src/main/java/com/example/purerestapi/controller/RestExceptionHandler.java b/src/main/java/com/example/purerestapi/controller/RestExceptionHandler.java new file mode 100644 index 0000000..7ab4928 --- /dev/null +++ b/src/main/java/com/example/purerestapi/controller/RestExceptionHandler.java @@ -0,0 +1,14 @@ +package com.example.purerestapi.controller; + +import com.example.purerestapi.exception.UserNotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@org.springframework.web.bind.annotation.ControllerAdvice +public class RestExceptionHandler { + + @ExceptionHandler(value = {UserNotFoundException.class}) + protected ResponseEntity handleConflict(RuntimeException ex) { + return ResponseEntity.badRequest().body(ex.getMessage()); + } +} diff --git a/src/main/java/com/example/purerestapi/controller/UserController.java b/src/main/java/com/example/purerestapi/controller/UserController.java new file mode 100644 index 0000000..1ad3d2b --- /dev/null +++ b/src/main/java/com/example/purerestapi/controller/UserController.java @@ -0,0 +1,89 @@ +package com.example.purerestapi.controller; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import com.example.purerestapi.controller.assembler.NoteAssembler; +import com.example.purerestapi.controller.assembler.UserAssembler; +import com.example.purerestapi.entity.Note; +import com.example.purerestapi.entity.User; +import com.example.purerestapi.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Controller +@ResponseBody +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + private final UserAssembler userAssembler; + private final NoteAssembler noteAssembler; + + @GetMapping + public CollectionModel> getAll() { + var users = userService.getAll().stream() + .map(userAssembler::toModel) + .toList(); + return CollectionModel.of( + users, + linkTo(methodOn(UserController.class).getAll()).withSelfRel() + ); + } + + @GetMapping("/{id}") + public EntityModel getUser(@PathVariable String id) { + var user = userService.getUser(id); + + return userAssembler.toModel(user); + } + + @PostMapping + public ResponseEntity> addUser(@RequestBody User user) { + userService.addUser(user); + EntityModel entityModel = userAssembler.toModel(user); + + return ResponseEntity + .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) + .body(entityModel); + } + + @DeleteMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void deleteUser(@PathVariable String id) { + userService.deleteUser(id); + } + + @PostMapping("/{id}/notes") + public EntityModel addNote(@PathVariable(name = "id") String userId, @RequestBody Note note) { + var user = userService.addNote(userId, note); + + return userAssembler.toModel(user); + } + + @GetMapping("/{id}/notes") + public CollectionModel> getUserNotes(@PathVariable(name = "id") String userId) { + var notes = userService.getAllNotes(userId).stream() + .map(noteAssembler::toModel) + .toList(); + + return CollectionModel.of( + notes, + linkTo(methodOn(UserController.class).getUserNotes(userId)).withSelfRel() + ); + } +} diff --git a/src/main/java/com/example/purerestapi/controller/assembler/NoteAssembler.java b/src/main/java/com/example/purerestapi/controller/assembler/NoteAssembler.java new file mode 100644 index 0000000..eb3a6b8 --- /dev/null +++ b/src/main/java/com/example/purerestapi/controller/assembler/NoteAssembler.java @@ -0,0 +1,23 @@ +package com.example.purerestapi.controller.assembler; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import com.example.purerestapi.controller.UserController; +import com.example.purerestapi.entity.Note; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.stereotype.Component; + +@Component +public class NoteAssembler implements RepresentationModelAssembler> { + + @Override + public EntityModel toModel(Note note) { + return EntityModel.of( + note, + linkTo(methodOn(UserController.class).getUserNotes(note.getUserUuid())).withSelfRel(), + linkTo(methodOn(UserController.class).getUser(note.getUserUuid())).withRel("user") + ); + } +} diff --git a/src/main/java/com/example/purerestapi/controller/assembler/UserAssembler.java b/src/main/java/com/example/purerestapi/controller/assembler/UserAssembler.java new file mode 100644 index 0000000..6bed2d8 --- /dev/null +++ b/src/main/java/com/example/purerestapi/controller/assembler/UserAssembler.java @@ -0,0 +1,24 @@ +package com.example.purerestapi.controller.assembler; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import com.example.purerestapi.controller.UserController; +import com.example.purerestapi.entity.User; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.stereotype.Component; + +@Component +public class UserAssembler implements RepresentationModelAssembler> { + + @Override + public EntityModel toModel(User user) { + return EntityModel.of( + user, + linkTo(methodOn(UserController.class).getUser(user.getId())).withSelfRel(), + linkTo(methodOn(UserController.class).getAll()).withRel("allUsers"), + linkTo(methodOn(UserController.class).getUserNotes(user.getId())).withRel("notes") + ); + } +} diff --git a/src/main/java/com/example/purerestapi/entity/Note.java b/src/main/java/com/example/purerestapi/entity/Note.java new file mode 100644 index 0000000..5c5ef22 --- /dev/null +++ b/src/main/java/com/example/purerestapi/entity/Note.java @@ -0,0 +1,13 @@ +package com.example.purerestapi.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +@Data +public class Note { + + @JsonIgnore + private String userUuid; + private String subject; + private String text; +} diff --git a/src/main/java/com/example/purerestapi/entity/User.java b/src/main/java/com/example/purerestapi/entity/User.java new file mode 100644 index 0000000..ef654a0 --- /dev/null +++ b/src/main/java/com/example/purerestapi/entity/User.java @@ -0,0 +1,17 @@ +package com.example.purerestapi.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class User { + + private String id; + private String firstname; + private String surname; + @JsonIgnore + private List notes; +} diff --git a/src/main/java/com/example/purerestapi/exception/UserNotFoundException.java b/src/main/java/com/example/purerestapi/exception/UserNotFoundException.java new file mode 100644 index 0000000..9cfe0dc --- /dev/null +++ b/src/main/java/com/example/purerestapi/exception/UserNotFoundException.java @@ -0,0 +1,8 @@ +package com.example.purerestapi.exception; + +public class UserNotFoundException extends RuntimeException { + + public UserNotFoundException() { + super("User Not Found"); + } +} diff --git a/src/main/java/com/example/purerestapi/repository/UserRepository.java b/src/main/java/com/example/purerestapi/repository/UserRepository.java new file mode 100644 index 0000000..14a1e0a --- /dev/null +++ b/src/main/java/com/example/purerestapi/repository/UserRepository.java @@ -0,0 +1,36 @@ +package com.example.purerestapi.repository; + +import com.example.purerestapi.entity.User; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.springframework.stereotype.Repository; + +@Repository +public class UserRepository { + + private final static Map users = new HashMap<>(); + + + public List getAll() { + return users.values().stream().toList(); + } + public void addUser(User user) { + if (user.getId() == null) { + user.setId(UUID.randomUUID().toString()); + } + if (!users.containsKey(user.getId())) { + users.put(user.getId(), user); + } + } + + public Optional getUser(String id) { + return Optional.ofNullable(users.get(id)); + } + + public void deleteUser(String id) { + users.remove(id); + } +} diff --git a/src/main/java/com/example/purerestapi/service/UserService.java b/src/main/java/com/example/purerestapi/service/UserService.java new file mode 100644 index 0000000..ff3e87b --- /dev/null +++ b/src/main/java/com/example/purerestapi/service/UserService.java @@ -0,0 +1,48 @@ +package com.example.purerestapi.service; + +import com.example.purerestapi.exception.UserNotFoundException; +import com.example.purerestapi.entity.Note; +import com.example.purerestapi.entity.User; +import com.example.purerestapi.repository.UserRepository; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + public void addUser(User user) { + userRepository.addUser(user); + } + + public User getUser(String id) { + return userRepository.getUser(id).orElseThrow(UserNotFoundException::new); + } + + public void deleteUser(String id) { + userRepository.deleteUser(id); + } + + public List getAll() { + return userRepository.getAll(); + } + + public User addNote(String userUuid, Note note) { + var user = this.getUser(userUuid); + if (user.getNotes() == null) { + user.setNotes(new ArrayList<>()); + } + note.setUserUuid(userUuid); + user.getNotes().add(note); + return user; + } + + public List getAllNotes(String userUuid) { + var user = this.getUser(userUuid); + return user.getNotes() == null ? Collections.emptyList() : user.getNotes(); + } +} From 3178a1a845338ce1b1b0a15a4e442d5fcad5a9c6 Mon Sep 17 00:00:00 2001 From: "mykola.filimonov" Date: Mon, 23 Oct 2023 10:23:11 +0300 Subject: [PATCH 2/2] HT-14 comment fixes --- .../controller/UserController.java | 21 ++++++------------- .../com/example/purerestapi/entity/User.java | 17 +++++++++++++++ .../repository/UserRepository.java | 3 ++- .../purerestapi/service/UserService.java | 6 ++---- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/purerestapi/controller/UserController.java b/src/main/java/com/example/purerestapi/controller/UserController.java index 1ad3d2b..bb3c75f 100644 --- a/src/main/java/com/example/purerestapi/controller/UserController.java +++ b/src/main/java/com/example/purerestapi/controller/UserController.java @@ -36,13 +36,9 @@ public class UserController { @GetMapping public CollectionModel> getAll() { - var users = userService.getAll().stream() - .map(userAssembler::toModel) - .toList(); - return CollectionModel.of( - users, - linkTo(methodOn(UserController.class).getAll()).withSelfRel() - ); + var users = userService.getAll(); + return userAssembler.toCollectionModel(users) + .add(linkTo(methodOn(UserController.class).getAll()).withSelfRel()); } @GetMapping("/{id}") @@ -77,13 +73,8 @@ public EntityModel addNote(@PathVariable(name = "id") String userId, @Requ @GetMapping("/{id}/notes") public CollectionModel> getUserNotes(@PathVariable(name = "id") String userId) { - var notes = userService.getAllNotes(userId).stream() - .map(noteAssembler::toModel) - .toList(); - - return CollectionModel.of( - notes, - linkTo(methodOn(UserController.class).getUserNotes(userId)).withSelfRel() - ); + var notes = userService.getAllNotes(userId); + return noteAssembler.toCollectionModel(notes) + .add(linkTo(methodOn(UserController.class).getUserNotes(userId)).withSelfRel()); } } diff --git a/src/main/java/com/example/purerestapi/entity/User.java b/src/main/java/com/example/purerestapi/entity/User.java index ef654a0..b632ad4 100644 --- a/src/main/java/com/example/purerestapi/entity/User.java +++ b/src/main/java/com/example/purerestapi/entity/User.java @@ -1,7 +1,11 @@ package com.example.purerestapi.entity; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -13,5 +17,18 @@ public class User { private String firstname; private String surname; @JsonIgnore + @Setter(AccessLevel.NONE) private List notes; + + public void addNote(Note note) { + if (notes == null) { + notes = new ArrayList<>(); + } + notes.add(note); + } + + public List getNotes() { + return Collections.unmodifiableList(notes); + } + } diff --git a/src/main/java/com/example/purerestapi/repository/UserRepository.java b/src/main/java/com/example/purerestapi/repository/UserRepository.java index 14a1e0a..24c3ce6 100644 --- a/src/main/java/com/example/purerestapi/repository/UserRepository.java +++ b/src/main/java/com/example/purerestapi/repository/UserRepository.java @@ -6,12 +6,13 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Repository; @Repository public class UserRepository { - private final static Map users = new HashMap<>(); + private final static Map users = new ConcurrentHashMap<>(); public List getAll() { diff --git a/src/main/java/com/example/purerestapi/service/UserService.java b/src/main/java/com/example/purerestapi/service/UserService.java index ff3e87b..589ae84 100644 --- a/src/main/java/com/example/purerestapi/service/UserService.java +++ b/src/main/java/com/example/purerestapi/service/UserService.java @@ -33,11 +33,9 @@ public List getAll() { public User addNote(String userUuid, Note note) { var user = this.getUser(userUuid); - if (user.getNotes() == null) { - user.setNotes(new ArrayList<>()); - } + note.setUserUuid(userUuid); - user.getNotes().add(note); + user.addNote(note); return user; }