String writeValue(T obj) throws JsonProcessingException {
+ return objectMapper.writeValueAsString(obj);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java b/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
new file mode 100644
index 0000000..4d5c29c
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
@@ -0,0 +1,24 @@
+package ru.javaops.bootjava.util;
+
+import lombok.experimental.UtilityClass;
+import ru.javaops.bootjava.error.IllegalRequestDataException;
+import ru.javaops.bootjava.model.BaseEntity;
+
+@UtilityClass
+public class ValidationUtil {
+
+ public static void checkNew(BaseEntity entity) {
+ if (!entity.isNew()) {
+ throw new IllegalRequestDataException(entity.getClass().getSimpleName() + " must be new (id=null)");
+ }
+ }
+
+ // Conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473)
+ public static void assureIdConsistent(BaseEntity entity, int id) {
+ if (entity.isNew()) {
+ entity.setId(id);
+ } else if (entity.id() != id) {
+ throw new IllegalRequestDataException(entity.getClass().getSimpleName() + " must has id=" + id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/AccountController.java b/src/main/java/ru/javaops/bootjava/web/AccountController.java
new file mode 100644
index 0000000..a43eb3e
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/web/AccountController.java
@@ -0,0 +1,113 @@
+package ru.javaops.bootjava.web;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.data.rest.webmvc.RepositoryLinksResource;
+import org.springframework.hateoas.EntityModel;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.hateoas.server.RepresentationModelProcessor;
+import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import ru.javaops.bootjava.AuthUser;
+import ru.javaops.bootjava.model.Role;
+import ru.javaops.bootjava.model.User;
+import ru.javaops.bootjava.repository.UserRepository;
+import ru.javaops.bootjava.util.ValidationUtil;
+
+import javax.validation.Valid;
+import java.net.URI;
+import java.util.EnumSet;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+
+/**
+ * Do not use {@link org.springframework.data.rest.webmvc.RepositoryRestController (BasePathAwareController}
+ * Bugs:
+ * NPE with http://localhost:8080/api/account
+ * data.rest.base-path missed in HAL links
+ * Two endpoints created
+ *
+ * RequestMapping("/${spring.data.rest.basePath}/account") give "Not enough variable values"
+ */
+@RestController
+@RequestMapping(AccountController.URL)
+@AllArgsConstructor
+@Slf4j
+@Tag(name = "Account Controller")
+public class AccountController implements RepresentationModelProcessor {
+ static final String URL = "/api/account";
+
+ @SuppressWarnings("unchecked")
+ private static final RepresentationModelAssemblerSupport> ASSEMBLER =
+ new RepresentationModelAssemblerSupport<>(AccountController.class, (Class>) (Class>) EntityModel.class) {
+ @Override
+ public EntityModel toModel(User user) {
+ return EntityModel.of(user, linkTo(AccountController.class).withSelfRel());
+ }
+ };
+
+ private final UserRepository userRepository;
+
+ @GetMapping(produces = MediaTypes.HAL_JSON_VALUE)
+ public EntityModel get(@AuthenticationPrincipal AuthUser authUser) {
+ log.info("get {}", authUser);
+ return ASSEMBLER.toModel(authUser.getUser());
+ }
+
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @CacheEvict(value = "users", key = "#authUser.username")
+ public void delete(@AuthenticationPrincipal AuthUser authUser) {
+ log.info("delete {}", authUser);
+ userRepository.deleteById(authUser.id());
+ }
+
+ @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(value = HttpStatus.CREATED)
+ public ResponseEntity> register(@Valid @RequestBody User user) {
+ log.info("register {}", user);
+ ValidationUtil.checkNew(user);
+ user.setRoles(EnumSet.of(Role.USER));
+ user = userRepository.save(user);
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path("/api/account")
+ .build().toUri();
+ return ResponseEntity.created(uriOfNewResource).body(ASSEMBLER.toModel(user));
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ @CachePut(value = "users", key = "#authUser.username")
+ public User update(@Valid @RequestBody User user, @AuthenticationPrincipal AuthUser authUser) {
+ log.info("update {} to {}", authUser, user);
+ User oldUser = authUser.getUser();
+ ValidationUtil.assureIdConsistent(user, oldUser.id());
+ user.setRoles(oldUser.getRoles());
+ if (user.getPassword() == null) {
+ user.setPassword(oldUser.getPassword());
+ }
+ return userRepository.save(user);
+ }
+
+/*
+ @GetMapping(value = "/pageDemo", produces = MediaTypes.HAL_JSON_VALUE)
+ public PagedModel> pageDemo(Pageable page, PagedResourcesAssembler pagedAssembler) {
+ Page users = userRepository.findAll(page);
+ return pagedAssembler.toModel(users, ASSEMBLER);
+ }
+*/
+
+ @Override
+ public RepositoryLinksResource process(RepositoryLinksResource resource) {
+ resource.add(linkTo(AccountController.class).withRel("account"));
+ return resource;
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java b/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java
new file mode 100644
index 0000000..d441662
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java
@@ -0,0 +1,38 @@
+package ru.javaops.bootjava.web.error;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.servlet.error.ErrorAttributes;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+import ru.javaops.bootjava.error.AppException;
+
+import java.util.Map;
+
+@RestControllerAdvice
+@AllArgsConstructor
+@Slf4j
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+ private final ErrorAttributes errorAttributes;
+
+ @ExceptionHandler(AppException.class)
+ public ResponseEntity