Skip to content
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

HT-14 implementation of Pure Rest API #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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,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();
}
}
Original file line number Diff line number Diff line change
@@ -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

Choose a reason for hiding this comment

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

Any reason to put here full qualified name with package?

public class RestExceptionHandler {

@ExceptionHandler(value = {UserNotFoundException.class})
protected ResponseEntity<Object> handleConflict(RuntimeException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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

Choose a reason for hiding this comment

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

You can use '@RestController' instead that is itself annotated with @controller and @responsebody.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good point, I have just done it as a test, and one more time show @RestController it is easier and more understandable than just annotation

@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

private final UserService userService;
private final UserAssembler userAssembler;
private final NoteAssembler noteAssembler;

@GetMapping
public CollectionModel<EntityModel<User>> getAll() {
var users = userService.getAll();
return userAssembler.toCollectionModel(users)
.add(linkTo(methodOn(UserController.class).getAll()).withSelfRel());
}

@GetMapping("/{id}")
public EntityModel<User> getUser(@PathVariable String id) {
var user = userService.getUser(id);

return userAssembler.toModel(user);
}

@PostMapping
public ResponseEntity<EntityModel<User>> addUser(@RequestBody User user) {
userService.addUser(user);
EntityModel<User> 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<User> addNote(@PathVariable(name = "id") String userId, @RequestBody Note note) {
var user = userService.addNote(userId, note);

return userAssembler.toModel(user);
}

@GetMapping("/{id}/notes")
public CollectionModel<EntityModel<Note>> getUserNotes(@PathVariable(name = "id") String userId) {
var notes = userService.getAllNotes(userId);
return noteAssembler.toCollectionModel(notes)
.add(linkTo(methodOn(UserController.class).getUserNotes(userId)).withSelfRel());
}
}
Original file line number Diff line number Diff line change
@@ -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<Note, EntityModel<Note>> {

@Override
public EntityModel<Note> toModel(Note note) {
return EntityModel.of(
note,
linkTo(methodOn(UserController.class).getUserNotes(note.getUserUuid())).withSelfRel(),
linkTo(methodOn(UserController.class).getUser(note.getUserUuid())).withRel("user")
);
}
}
Original file line number Diff line number Diff line change
@@ -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<User, EntityModel<User>> {

@Override
public EntityModel<User> 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")
);
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/example/purerestapi/entity/Note.java
Original file line number Diff line number Diff line change
@@ -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;
}
34 changes: 34 additions & 0 deletions src/main/java/com/example/purerestapi/entity/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;

@Getter
@Setter
public class User {

private String id;
private String firstname;
private String surname;
@JsonIgnore
@Setter(AccessLevel.NONE)
private List<Note> notes;

public void addNote(Note note) {
if (notes == null) {
notes = new ArrayList<>();
}
notes.add(note);
}

public List<Note> getNotes() {
return Collections.unmodifiableList(notes);

Choose a reason for hiding this comment

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

If notes field is null there will be NullPointerException thrown.

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.purerestapi.exception;

public class UserNotFoundException extends RuntimeException {

public UserNotFoundException() {
super("User Not Found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {

private final static Map<String, User> users = new ConcurrentHashMap<>();


public List<User> getAll() {
return users.values().stream().toList();
}
public void addUser(User user) {
if (user.getId() == null) {

Choose a reason for hiding this comment

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

How can you have here non-null user ID? It's create operation, right?

user.setId(UUID.randomUUID().toString());
}
if (!users.containsKey(user.getId())) {

Choose a reason for hiding this comment

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

It's an interesting part of the code in concept "what if there is already UUID in the map?". Do you expect duplicates?
If so, then better to make it in loop until unique UUID is found.

users.put(user.getId(), user);
Comment on lines +25 to +26

Choose a reason for hiding this comment

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

Better to use here putIfAbscent method which is atomical in ConcurrentHashMap and thus there will be no race condition in between of containsKey operation and put operation.

}
}

public Optional<User> getUser(String id) {
return Optional.ofNullable(users.get(id));
}

public void deleteUser(String id) {
users.remove(id);
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/example/purerestapi/service/UserService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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<User> getAll() {
return userRepository.getAll();
}

public User addNote(String userUuid, Note note) {
var user = this.getUser(userUuid);

note.setUserUuid(userUuid);
user.addNote(note);
return user;
}

public List<Note> getAllNotes(String userUuid) {
var user = this.getUser(userUuid);
return user.getNotes() == null ? Collections.emptyList() : user.getNotes();

Choose a reason for hiding this comment

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

Better to handle this case in one place - getNotes method itself.

}
}