Refactor controllers and exception handling (#8)

* Refactored exceptions and responses

* Extracted exceptions to external api library

* Refactored exceptions and extracted models to external library

* Fixed test after refactor

* Extracted validator to api library

* Implemented session error handler

* Upgraded notifications

* Implemented global exception handler for control panel
This commit is contained in:
Andrey Terentev 2024-01-16 16:29:44 +07:00 committed by GitHub
parent 01518f4054
commit 7566f24d36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 1423 additions and 881 deletions

16
api/build.gradle Normal file
View File

@ -0,0 +1,16 @@
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
test {
useJUnitPlatform()
}

View File

@ -0,0 +1,10 @@
package ru.dragonestia.picker.api.exception;
import java.util.Map;
public abstract class ApiException extends RuntimeException {
public abstract String getErrorId();
public abstract void appendDetailsToErrorResponse(Map<String, String> details);
}

View File

@ -0,0 +1,36 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class ExceptionFactory {
private final static Map<String, Constructor> factory = init();
private ExceptionFactory() {}
private static Map<String, Constructor> init() {
var factory = new HashMap<String, Constructor>();
factory.put(InvalidNodeIdentifierException.ERROR_ID, InvalidNodeIdentifierException::new);
factory.put(InvalidRoomIdentifierException.ERROR_ID, InvalidRoomIdentifierException::new);
factory.put(InvalidUsernamesException.ERROR_ID, InvalidUsernamesException::new);
factory.put(NodeAlreadyExistException.ERROR_ID, NodeAlreadyExistException::new);
factory.put(NodeNotFoundException.ERROR_ID, NodeNotFoundException::new);
factory.put(NoRoomsAvailableException.ERROR_ID, NoRoomsAvailableException::new);
factory.put(RoomAlreadyExistException.ERROR_ID, RoomAlreadyExistException::new);
factory.put(RoomAreFullException.ERROR_ID, RoomAreFullException::new);
factory.put(RoomNotFoundException.ERROR_ID, RoomNotFoundException::new);
return factory;
}
public static ApiException of(ErrorResponse errorResponse) {
return factory.get(errorResponse.errorId()).apply(errorResponse);
}
private interface Constructor extends Function<ErrorResponse, ApiException> {}
}

View File

@ -0,0 +1,39 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class InvalidNodeIdentifierException extends ApiException {
public static final String ERROR_ID = "err.node.invalid_identifier";
private final String nodeId;
public InvalidNodeIdentifierException(String nodeId) {
this.nodeId = nodeId;
}
public InvalidNodeIdentifierException(ErrorResponse errorResponse) {
this(errorResponse.details().get("identifier"));
}
@Override
public String getMessage() {
return "Invalid node identifier. Taken '" + nodeId + "'";
}
public String getNodeId() {
return nodeId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("identifier", getNodeId());
}
}

View File

@ -0,0 +1,46 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class InvalidRoomIdentifierException extends ApiException {
public static final String ERROR_ID = "err.room.invalid_identifier";
private final String nodeId;
private final String roomId;
public InvalidRoomIdentifierException(String nodeId, String roomId) {
this.nodeId = nodeId;
this.roomId = roomId;
}
public InvalidRoomIdentifierException(ErrorResponse errorResponse) {
this(errorResponse.details().get("nodeId"), errorResponse.details().get("roomId"));
}
@Override
public String getMessage() {
return "Invalid room identifier. Taken '" + roomId + "'";
}
public String getRoomId() {
return roomId;
}
public String getNodeId() {
return nodeId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("nodeId", getNodeId());
details.put("roomId", getRoomId());
}
}

View File

@ -0,0 +1,50 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.*;
import java.util.function.Function;
public final class InvalidUsernamesException extends ApiException {
public static final String ERROR_ID = "err.user.invalid_identifier";
private final List<String> givenUsernames;
private final List<String> invalidUsernames;
public InvalidUsernamesException(List<String> givenUsernames, List<String> invalidUsernames) {
this.givenUsernames = givenUsernames;
this.invalidUsernames = invalidUsernames;
}
public InvalidUsernamesException(ErrorResponse errorResponse) {
this(Arrays.stream(errorResponse.details().get("givenUsernames").split(",")).toList(),
Arrays.stream(errorResponse.details().get("invalidUsernames").split(",")).toList());
}
@Override
public String getMessage() {
return "Users with invalid identifiers were found";
}
public List<String> getGivenUsernames() {
return givenUsernames;
}
public List<String> getInvalidUsernames() {
return invalidUsernames;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
Function<Collection<String>, String> toString = input -> String.join(",", input);
details.put("givenUsernames", toString.apply(getGivenUsernames()));
details.put("invalidUsernames", toString.apply(getInvalidUsernames()));
}
}

View File

@ -0,0 +1,39 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class NoRoomsAvailableException extends ApiException {
public static final String ERROR_ID = "err.room.no_available";
private final String nodeId;
public NoRoomsAvailableException(String nodeId) {
this.nodeId = nodeId;
}
public NoRoomsAvailableException(ErrorResponse errorResponse) {
this(errorResponse.details().get("nodeId"));
}
@Override
public String getMessage() {
return "There are no rooms available in node '" + nodeId + "'";
}
public String getNodeId() {
return nodeId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("nodeId", getNodeId());
}
}

View File

@ -0,0 +1,39 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class NodeAlreadyExistException extends ApiException {
public static final String ERROR_ID = "err.node.already_exists";
private final String nodeId;
public NodeAlreadyExistException(String nodeId) {
this.nodeId = nodeId;
}
public NodeAlreadyExistException(ErrorResponse errorResponse) {
this(errorResponse.details().get("nodeId"));
}
@Override
public String getMessage() {
return "Node with identifier '" + nodeId + "' is already exists";
}
public String getNodeId() {
return nodeId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("nodeId", getNodeId());
}
}

View File

@ -0,0 +1,39 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class NodeNotFoundException extends ApiException {
public static final String ERROR_ID = "err.node.not_found";
private final String nodeId;
public NodeNotFoundException(String nodeId) {
this.nodeId = nodeId;
}
public NodeNotFoundException(ErrorResponse errorResponse) {
this(errorResponse.details().get("node"));
}
@Override
public String getMessage() {
return "Node '" + nodeId + "' does not found";
}
public String getNodeId() {
return nodeId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("node", getNodeId());
}
}

View File

@ -0,0 +1,47 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class RoomAlreadyExistException extends ApiException {
public static final String ERROR_ID = "err.room.already_exists";
private final String nodeId;
private final String roomId;
public RoomAlreadyExistException(String nodeId, String roomId) {
this.nodeId = nodeId;
this.roomId = roomId;
}
public RoomAlreadyExistException(ErrorResponse errorResponse) {
this(errorResponse.details().get("nodeId"),
errorResponse.details().get("roomId"));
}
@Override
public String getMessage() {
return "Room with identifier '" + roomId + "' in node '" + nodeId + "' is already exists";
}
public String getNodeId() {
return nodeId;
}
public String getRoomId() {
return roomId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("nodeId", getNodeId());
details.put("roomId", getRoomId());
}
}

View File

@ -0,0 +1,47 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class RoomAreFullException extends ApiException {
public static final String ERROR_ID = "err.room.are_full";
private final String nodeId;
private final String roomId;
public RoomAreFullException(String nodeId, String roomId) {
this.nodeId = nodeId;
this.roomId = roomId;
}
public RoomAreFullException(ErrorResponse errorResponse) {
this(errorResponse.details().get("nodeId"),
errorResponse.details().get("roomId"));
}
@Override
public String getMessage() {
return "Room with identifier '" + roomId + "' in node '" + nodeId + "' are full";
}
public String getNodeId() {
return nodeId;
}
public String getRoomId() {
return roomId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("nodeId", getNodeId());
details.put("roomId", getRoomId());
}
}

View File

@ -0,0 +1,47 @@
package ru.dragonestia.picker.api.exception;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.Map;
public final class RoomNotFoundException extends ApiException {
public static final String ERROR_ID = "err.room.not_found";
private final String nodeId;
private final String roomId;
public RoomNotFoundException(String nodeId, String roomId) {
this.nodeId = nodeId;
this.roomId = roomId;
}
public RoomNotFoundException(ErrorResponse errorResponse) {
this(errorResponse.details().get("nodeId"),
errorResponse.details().get("roomId"));
}
@Override
public String getMessage() {
return "Room '" + roomId + "' in node '" + nodeId + "' does not found";
}
public String getNodeId() {
return nodeId;
}
public String getRoomId() {
return roomId;
}
@Override
public String getErrorId() {
return ERROR_ID;
}
@Override
public void appendDetailsToErrorResponse(Map<String, String> details) {
details.put("node", getNodeId());
details.put("room", getRoomId());
}
}

View File

@ -0,0 +1,39 @@
package ru.dragonestia.picker.api.model;
import ru.dragonestia.picker.api.model.type.PickingMode;
public class Node {
private String id;
private PickingMode mode;
private Node() {}
public Node(String id, PickingMode mode) {
this.id = id;
this.mode = mode;
}
public String getId() {
return id;
}
public PickingMode getMode() {
return mode;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object object) {
if (object == this) return true;
if (object == null) return false;
if (object instanceof Node other) {
return id.equals(other.id);
}
return false;
}
}

View File

@ -0,0 +1,73 @@
package ru.dragonestia.picker.api.model;
import java.beans.Transient;
public class Room {
public final static int INFINITE_SLOTS = -1;
private String id;
private String nodeId;
private int slots;
private String payload;
private boolean locked = false;
private Room() {}
public Room(String id, String nodeId, int slots, String payload) {
this.id = id;
this.nodeId = nodeId;
this.slots = slots;
this.payload = payload;
}
public Room(String id, Node node, int limit, String payload) {
this(id, node.getId(), limit, payload);
}
public String getId() {
return id;
}
public String getNodeId() {
return nodeId;
}
public int getSlots() {
return slots;
}
public String getPayload() {
return payload;
}
public boolean isLocked() {
return locked;
}
public void setLocked(boolean value) {
locked = value;
}
@Transient
public boolean isUnlimited() {
return slots == INFINITE_SLOTS;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object object) {
if (object == this) return true;
if (object == null) return false;
if (object instanceof Room other) {
return id.equals(other.id);
}
return false;
}
public record Short(String id, int slots, boolean locked) {}
}

View File

@ -1,8 +1,18 @@
package ru.dragonestia.picker.cp.model;
package ru.dragonestia.picker.api.model;
import lombok.NonNull;
public class User {
public record User(@NonNull String id) {
private String id;
private User() {}
public User(String id) {
this.id = id;
}
public String getId() {
return id;
}
@Override
public int hashCode() {

View File

@ -1,14 +1,17 @@
package ru.dragonestia.picker.cp.model.type;
package ru.dragonestia.picker.api.model.type;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum PickingMode {
SEQUENTIAL_FILLING("Sequential filling"),
ROUND_ROBIN("Round Robin"),
LEAST_PICKED("Least Picked");
private final String name;
PickingMode(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@ -0,0 +1,19 @@
package ru.dragonestia.picker.api.repository;
import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.api.model.Node;
import java.util.List;
import java.util.Optional;
public interface NodeRepository {
void register(Node node) throws InvalidNodeIdentifierException, NodeAlreadyExistException;
List<Node> all();
Optional<Node> find(String nodeId);
void remove(String nodeId);
}

View File

@ -0,0 +1,23 @@
package ru.dragonestia.picker.api.repository;
import ru.dragonestia.picker.api.exception.*;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.model.Room;
import java.util.List;
import java.util.Optional;
public interface RoomRepository {
void register(Room room) throws NodeNotFoundException, InvalidRoomIdentifierException, RoomAlreadyExistException;
void remove(Room room) throws NodeNotFoundException;
void remove(Node node, Room.Short room) throws NodeNotFoundException;
List<Room.Short> all(Node node) throws NodeNotFoundException;
Optional<Room> find(Node node, String roomId) throws NodeNotFoundException;
void lock(Room room, boolean value) throws NodeNotFoundException, RoomNotFoundException;
}

View File

@ -0,0 +1,19 @@
package ru.dragonestia.picker.api.repository;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.model.User;
import java.util.Collection;
import java.util.List;
public interface UserRepository {
void linkWithRoom(Room room, Collection<User> users, boolean force) throws NodeNotFoundException, RoomNotFoundException, RoomAreFullException;
void unlinkFromRoom(Room room, Collection<User> users) throws NodeNotFoundException, RoomNotFoundException;
List<User> all(Room room) throws NodeNotFoundException, RoomNotFoundException;
}

View File

@ -0,0 +1,5 @@
package ru.dragonestia.picker.api.repository.response;
import java.util.Map;
public record ErrorResponse(String errorId, String message, Map<String, String> details) {}

View File

@ -0,0 +1,3 @@
package ru.dragonestia.picker.api.repository.response;
public record LinkUsersWithRoomResponse(int usedSlots, int totalSlots) {}

View File

@ -0,0 +1,5 @@
package ru.dragonestia.picker.api.repository.response;
import ru.dragonestia.picker.api.model.Node;
public record NodeDetailsResponse(Node node) {}

View File

@ -0,0 +1,7 @@
package ru.dragonestia.picker.api.repository.response;
import ru.dragonestia.picker.api.model.Node;
import java.util.List;
public record NodeListResponse(List<Node> nodes) {}

View File

@ -0,0 +1,5 @@
package ru.dragonestia.picker.api.repository.response;
import ru.dragonestia.picker.api.model.Room;
public record RoomInfoResponse(Room room) {}

View File

@ -0,0 +1,7 @@
package ru.dragonestia.picker.api.repository.response;
import ru.dragonestia.picker.api.model.Room;
import java.util.List;
public record RoomListResponse(String node, List<Room.Short> rooms) {}

View File

@ -1,6 +1,6 @@
package ru.dragonestia.picker.controller.response;
package ru.dragonestia.picker.api.repository.response;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.api.model.User;
import java.util.List;

View File

@ -0,0 +1,18 @@
package ru.dragonestia.picker.api.utils;
public class ValidateIdentifier {
private ValidateIdentifier() {}
public static boolean forNode(String nodeId) {
return nodeId.matches("^[a-z\\d-]+$");
}
public static boolean forRoom(String roomId) {
return roomId.matches("^[a-z\\d-]+$");
}
public static boolean forUser(String username) {
return username.matches("^[aA-zZ\\d-.\\s:@_;]+$");
}
}

View File

@ -10,6 +10,7 @@ configurations {
}
dependencies {
implementation project(":api")
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'

View File

@ -7,11 +7,11 @@ import org.springframework.context.annotation.Profile;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.interceptor.DebugInterceptor;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository;

View File

@ -0,0 +1,65 @@
package ru.dragonestia.picker.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import ru.dragonestia.picker.api.exception.*;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.util.HashMap;
@RestControllerAdvice
public class ExceptionHandlerController {
@ExceptionHandler(NodeNotFoundException.class)
ResponseEntity<?> nodeNotFound(NodeNotFoundException ex) {
return create(404, ex);
}
@ExceptionHandler(RoomNotFoundException.class)
ResponseEntity<?> roomNotFound(RoomNotFoundException ex) {
return create(404, ex);
}
@ExceptionHandler(InvalidUsernamesException.class)
ResponseEntity<?> invalidUsernames(InvalidUsernamesException ex) {
return create(400, ex);
}
@ExceptionHandler(InvalidNodeIdentifierException.class)
ResponseEntity<?> invalidNodeIdentifier(InvalidNodeIdentifierException ex) {
return create(400, ex);
}
@ExceptionHandler(InvalidRoomIdentifierException.class)
ResponseEntity<?> invalidRoomIdentifier(InvalidRoomIdentifierException ex) {
return create(400, ex);
}
@ExceptionHandler(NodeAlreadyExistException.class)
ResponseEntity<?> nodeAlreadyExists(NodeAlreadyExistException ex) {
return create(400, ex);
}
@ExceptionHandler(RoomAlreadyExistException.class)
ResponseEntity<?> roomAlreadyExists(RoomAlreadyExistException ex) {
return create(400, ex);
}
@ExceptionHandler(RoomAreFullException.class)
ResponseEntity<?> roomAreFull(RoomAreFullException ex) {
return create(400, ex);
}
@ExceptionHandler(NoRoomsAvailableException.class)
ResponseEntity<?> noRoomsAvailable(NoRoomsAvailableException ex) {
return create(400, ex);
}
private ResponseEntity<ErrorResponse> create(int code, ApiException ex) {
var details = new HashMap<String, String>();
ex.appendDetailsToErrorResponse(details);
return ResponseEntity.status(code).body(new ErrorResponse(ex.getErrorId(), ex.getMessage(), details));
}
}

View File

@ -1,22 +1,18 @@
package ru.dragonestia.picker.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.NodeDetailsResponse;
import ru.dragonestia.picker.controller.response.NodeListResponse;
import ru.dragonestia.picker.controller.response.NodeRegisterResponse;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.api.repository.response.NodeDetailsResponse;
import ru.dragonestia.picker.api.repository.response.NodeListResponse;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.LinkedList;
import java.util.Optional;
import java.util.Arrays;
@RestController
@RequestMapping("/nodes")
@ -25,47 +21,35 @@ public class NodeController {
private final NodeService nodeService;
private final RoomService roomService;
private final NamingValidator namingValidator;
@GetMapping
NodeListResponse allNodes() {
return new NodeListResponse(nodeService.all());
return new NodeListResponse(nodeService.all().stream().map(Node::toResponseObject).toList());
}
@PostMapping
NodeRegisterResponse registerNode(@RequestParam(name = "nodeId") String nodeId,
ResponseEntity<?> registerNode(@RequestParam(name = "nodeId") String nodeId,
@RequestParam(name = "method") PickingMode method) {
try {
nodeService.create(new Node(nodeId, method));
} catch (IllegalArgumentException ex) {
return new NodeRegisterResponse(false, ex.getMessage());
} catch (Error error) {
new NodeRegisterResponse(false, error.getMessage());
}
return new NodeRegisterResponse(true, "");
nodeService.create(new Node(nodeId, method));
return ResponseEntity.ok().build();
}
@GetMapping("/{nodeId}")
ResponseEntity<NodeDetailsResponse> nodeDetails(@PathVariable("nodeId") String nodeId) {
if (!NamingValidator.validateNodeId(nodeId)) {
return new ResponseEntity<>(HttpStatusCode.valueOf(404));
}
namingValidator.validateNodeId(nodeId);
var nodeOpt = nodeService.find(nodeId);
return nodeOpt.map(node -> ResponseEntity.ok(new NodeDetailsResponse(node)))
.orElseGet(() -> new ResponseEntity<>(HttpStatusCode.valueOf(404)));
return nodeService.find(nodeId)
.map(node -> ResponseEntity.ok(new NodeDetailsResponse(node.toResponseObject())))
.orElseThrow(() -> new NodeNotFoundException(nodeId));
}
@DeleteMapping("/{nodeId}")
ResponseEntity<?> removeNode(@PathVariable("nodeId") String nodeId) {
if (!NamingValidator.validateNodeId(nodeId)) {
return ResponseEntity.ok().build();
}
var nodeOpt = nodeService.find(nodeId);
nodeOpt.ifPresent(nodeService::remove);
namingValidator.validateNodeId(nodeId);
nodeService.find(nodeId).ifPresent(nodeService::remove);
return ResponseEntity.ok().build();
}
@ -73,30 +57,11 @@ public class NodeController {
ResponseEntity<?> pickRoom(@PathVariable("nodeId") String nodeId,
@RequestParam(name = "userIds") String userIds) {
if (!NamingValidator.validateNodeId(nodeId)) {
return new ResponseEntity<>(HttpStatusCode.valueOf(404));
}
namingValidator.validateNodeId(nodeId);
var nodeOpt = nodeService.find(nodeId);
if (nodeOpt.isEmpty()) {
return new ResponseEntity<>(HttpStatusCode.valueOf(404));
}
var node = nodeOpt.get();
var list = new LinkedList<User>();
for (var username: userIds.split(",")) { // TODO: create warnings about invalid usernames
if (!NamingValidator.validateUserId(username)) continue;
list.add(new User(username));
}
Room room;
try {
room = roomService.pickAvailable(node, list);
} catch (RuntimeException ex) {
return new ResponseEntity<>(HttpStatusCode.valueOf(409));
}
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
var room = roomService.pickAvailable(node, users);
return ResponseEntity.ok(room); // TODO: make other json schema
}

View File

@ -4,17 +4,16 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.RoomInfoResponse;
import ru.dragonestia.picker.controller.response.RoomListResponse;
import ru.dragonestia.picker.controller.response.RoomRegisterResponse;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.api.repository.response.RoomInfoResponse;
import ru.dragonestia.picker.api.repository.response.RoomListResponse;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.Objects;
@Log4j2
@RestController
@RequestMapping("/nodes/{nodeId}/rooms")
@ -23,49 +22,40 @@ public class RoomController {
private final NodeService nodeService;
private final RoomService roomService;
private final NamingValidator namingValidator;
@GetMapping
ResponseEntity<RoomListResponse> all(@PathVariable(name = "nodeId") String nodeId) {
var nodeOpt = nodeService.find(nodeId);
return nodeOpt.map(node -> ResponseEntity.ok(new RoomListResponse(nodeId,
roomService.all(node).stream()
.map(room -> new RoomListResponse.RoomDTO(room.getId(), room.getSlots().getSlots(), room.isLocked()))
.toList()
))).orElseGet(() -> ResponseEntity.notFound().build());
return nodeService.find(nodeId)
.map(node -> ResponseEntity.ok(new RoomListResponse(nodeId,
roomService.all(node).stream()
.map(room -> new ru.dragonestia.picker.api.model.Room.Short(room.getId(), room.getSlots().getSlots(), room.isLocked()))
.toList()
))).orElseThrow(() -> new NodeNotFoundException(nodeId));
}
@PostMapping
ResponseEntity<RoomRegisterResponse> register(@PathVariable(name = "nodeId") String nodeId,
@RequestParam(name = "roomId") String roomId,
@RequestParam(name = "slots") int slots,
@RequestParam(name = "payload") String payload,
@RequestParam(name = "locked", defaultValue = "false") boolean locked) {
ResponseEntity<?> register(@PathVariable(name = "nodeId") String nodeId,
@RequestParam(name = "roomId") String roomId,
@RequestParam(name = "slots") int slots,
@RequestParam(name = "payload") String payload,
@RequestParam(name = "locked", defaultValue = "false") boolean locked) {
var nodeOpt = nodeService.find(nodeId);
if (nodeOpt.isEmpty()) {
return ResponseEntity.status(404)
.body(new RoomRegisterResponse(false, "Node does not exist"));
}
var room = Room.create(roomId, nodeOpt.get(), SlotLimit.of(slots), payload);
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
var room = Room.create(roomId, node, SlotLimit.of(slots), payload);
room.setLocked(locked);
try {
roomService.create(room);
return ResponseEntity.ok(new RoomRegisterResponse(true, ""));
} catch (Error error) {
return ResponseEntity.status(400).body(new RoomRegisterResponse(false, error.getMessage()));
} catch (Exception ex) {
return ResponseEntity.status(500).body(new RoomRegisterResponse(false, ex.getMessage()));
}
roomService.create(room);
return ResponseEntity.ok().build();
}
@DeleteMapping("/{roomId}")
ResponseEntity<?> remove(@PathVariable("nodeId") String nodeId,
@PathVariable("roomId") String roomId) {
if (!NamingValidator.validateNodeId(nodeId) || !NamingValidator.validateRoomId(roomId)) {
return ResponseEntity.ok().build();
}
namingValidator.validateNodeId(nodeId);
namingValidator.validateRoomId(nodeId, roomId);
var nodeOpt = nodeService.find(nodeId);
nodeOpt.flatMap(node -> roomService.find(node, roomId))
@ -77,40 +67,26 @@ public class RoomController {
@GetMapping("/{roomId}")
ResponseEntity<RoomInfoResponse> info(@PathVariable("nodeId") String nodeId,
@PathVariable("roomId") String roomId) {
if (!NamingValidator.validateNodeId(nodeId) || !NamingValidator.validateRoomId(roomId)) {
return ResponseEntity.ok().build();
}
var nodeOpt = nodeService.find(nodeId);
if (nodeOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
namingValidator.validateNodeId(nodeId);
namingValidator.validateRoomId(nodeId, roomId);
var roomOpt = roomService.find(Objects.requireNonNull(nodeOpt.get()), roomId);
return roomOpt.map(room -> ResponseEntity.ok(new RoomInfoResponse(room)))
.orElseGet(() -> ResponseEntity.notFound().build());
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
return roomService.find(node, roomId)
.map(room -> ResponseEntity.ok(new RoomInfoResponse(room.toResponseObject())))
.orElseThrow(() -> new RoomNotFoundException(nodeId, roomId));
}
@PostMapping("/{roomId}/lock")
@PutMapping("/{roomId}/lock")
ResponseEntity<Boolean> lockBucket(@PathVariable("nodeId") String nodeId,
@PathVariable("roomId") String roomId,
@RequestParam(name = "newState") boolean value) {
@PathVariable("roomId") String roomId,
@RequestParam(name = "newState") boolean value) {
if (!NamingValidator.validateNodeId(nodeId) || !NamingValidator.validateRoomId(roomId)) {
return ResponseEntity.notFound().build();
}
namingValidator.validateNodeId(nodeId);
namingValidator.validateRoomId(nodeId, roomId);
var nodeOpt = nodeService.find(nodeId);
if (nodeOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
var roomOpt = roomService.find(Objects.requireNonNull(nodeOpt.get()), roomId);
if (roomOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
var room = roomOpt.get();
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
var room = roomService.find(node, roomId).orElseThrow(() -> new RoomNotFoundException(nodeId, roomId));
room.setLocked(value);
return ResponseEntity.ok(true);
}

View File

@ -3,8 +3,10 @@ package ru.dragonestia.picker.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.RoomUserListResponse;
import ru.dragonestia.picker.controller.response.LinkUsersWithRoomResponse;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.api.repository.response.LinkUsersWithRoomResponse;
import ru.dragonestia.picker.api.repository.response.RoomUserListResponse;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
@ -13,8 +15,7 @@ import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.service.UserService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Arrays;
@RequiredArgsConstructor
@RestController
@ -24,21 +25,15 @@ public class UserRoomController {
private final NodeService nodeService;
private final RoomService roomService;
private final UserService userService;
private final NamingValidator namingValidator;
@GetMapping
ResponseEntity<RoomUserListResponse> usersInsideRoom(@PathVariable(name = "nodeId") String nodeId,
@PathVariable(name = "roomId") String bucketId) {
Room room;
try {
var temp = getNodeAndRoom(nodeId, bucketId);
room = temp.room();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
@PathVariable(name = "roomId") String roomId) {
var room = getNodeAndRoom(nodeId, roomId).room();
var users = userService.getRoomUsers(room);
return ResponseEntity.ok(new RoomUserListResponse(room.getSlots().getSlots(), users.size(), users));
return ResponseEntity.ok(new RoomUserListResponse(room.getSlots().getSlots(), users.size(), users.stream().map(User::toResponseObject).toList()));
}
@PostMapping
@ -47,28 +42,10 @@ public class UserRoomController {
@RequestParam(name = "userIds") String userIds,
@RequestParam(name = "force") boolean force) {
Room room;
try {
var temp = getNodeAndRoom(nodeId, roomId);
room = temp.room();
} catch (Error error) {
return ResponseEntity.status(404).body(new LinkUsersWithRoomResponse(false, error.getMessage(), -1, -1));
}
var list = new LinkedList<User>();
for (var username: userIds.split(",")) { // TODO: create warnings about invalid usernames
if (!NamingValidator.validateUserId(username)) continue;
list.add(new User(username));
}
try {
int usedSlots = userService.linkUsersWithRoom(room, list, force);
return ResponseEntity.ok(new LinkUsersWithRoomResponse(true, "Success", usedSlots, room.getSlots().getSlots()));
} catch (Error error) {
return ResponseEntity.status(400).body(new LinkUsersWithRoomResponse(false, error.getMessage(), -1, -1));
}
var room = getNodeAndRoom(nodeId, roomId).room();
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
var usedSlots = userService.linkUsersWithRoom(room, users, force);
return ResponseEntity.ok(new LinkUsersWithRoomResponse(usedSlots, room.getSlots().getSlots()));
}
@DeleteMapping
@ -76,43 +53,21 @@ public class UserRoomController {
@PathVariable(name = "roomId") String roomId,
@RequestParam(name = "userIds") String userIds) {
Room room;
try {
var temp = getNodeAndRoom(nodeId, roomId);
room = temp.room();
var list = new LinkedList<User>();
for (var username: userIds.split(",")) { // TODO: create warnings about invalid usernames
if (!NamingValidator.validateUserId(username)) continue;
list.add(new User(username));
}
userService.unlinkUsersFromRoom(room, list);
} catch (Error error) {
return ResponseEntity.notFound().build();
}
var room = getNodeAndRoom(nodeId, roomId).room();
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
userService.unlinkUsersFromRoom(room, users);
return ResponseEntity.ok().build();
}
private record NodeAndRoom(Node node, Room room) {}
private NodeAndRoom getNodeAndRoom(String nodeId, String roomId) {
if (!NamingValidator.validateNodeId(nodeId) || !NamingValidator.validateRoomId(roomId)) {
throw new Error();
}
namingValidator.validateNodeId(nodeId);
namingValidator.validateRoomId(nodeId, roomId);
var nodeOpt = nodeService.find(nodeId);
if (nodeOpt.isEmpty()) {
throw new Error();
}
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
var room = roomService.find(node, roomId).orElseThrow(() -> new RoomNotFoundException(nodeId, roomId));
var roomOpt = roomService.find(Objects.requireNonNull(nodeOpt.get()), roomId);
if (roomOpt.isEmpty()) {
throw new Error();
}
return new NodeAndRoom(nodeOpt.get(), roomOpt.get());
return new NodeAndRoom(node, room);
}
}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.picker.controller.response;
public record LinkUsersWithRoomResponse(boolean success, String message, int usedSlots, int totalSlots) {}

View File

@ -1,5 +0,0 @@
package ru.dragonestia.picker.controller.response;
import ru.dragonestia.picker.model.Node;
public record NodeDetailsResponse(Node node) {}

View File

@ -1,7 +0,0 @@
package ru.dragonestia.picker.controller.response;
import ru.dragonestia.picker.model.Node;
import java.util.List;
public record NodeListResponse(List<Node> nodes) {}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.picker.controller.response;
public record NodeRegisterResponse(boolean success, String message) {}

View File

@ -1,5 +0,0 @@
package ru.dragonestia.picker.controller.response;
import ru.dragonestia.picker.model.Room;
public record RoomInfoResponse(Room room) {}

View File

@ -1,8 +0,0 @@
package ru.dragonestia.picker.controller.response;
import java.util.List;
public record RoomListResponse(String node, List<RoomDTO> rooms) {
public record RoomDTO(String id, int slots, boolean locked) {}
}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.picker.controller.response;
public record RoomRegisterResponse(boolean success, String message) {}

View File

@ -1,7 +1,7 @@
package ru.dragonestia.picker.model;
import lombok.NonNull;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.api.model.type.PickingMode;
public record Node(@NonNull String id, @NonNull PickingMode mode) {
@ -19,4 +19,8 @@ public record Node(@NonNull String id, @NonNull PickingMode mode) {
}
return false;
}
public ru.dragonestia.picker.api.model.Node toResponseObject() {
return new ru.dragonestia.picker.api.model.Node(id, mode);
}
}

View File

@ -43,4 +43,10 @@ public class Room {
}
return false;
}
public ru.dragonestia.picker.api.model.Room toResponseObject() {
var result = new ru.dragonestia.picker.api.model.Room(id, nodeId, slots.getSlots(), payload);
result.setLocked(locked);
return result;
}
}

View File

@ -18,4 +18,8 @@ public record User(@NonNull String id) {
}
return false;
}
public ru.dragonestia.picker.api.model.User toResponseObject() {
return new ru.dragonestia.picker.api.model.User(id);
}
}

View File

@ -1,7 +0,0 @@
package ru.dragonestia.picker.model.type;
public enum PickingMode {
SEQUENTIAL_FILLING,
ROUND_ROBIN,
LEAST_PICKED,
}

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import java.util.List;
@ -7,7 +8,7 @@ import java.util.Optional;
public interface NodeRepository {
void create(Node node);
void create(Node node) throws NodeAlreadyExistException;
void delete(Node node);

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
@ -10,7 +11,7 @@ import java.util.Optional;
public interface RoomRepository {
void create(Room room);
void create(Room room) throws RoomAlreadyExistException;
void remove(Room room);

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
@ -9,7 +10,7 @@ import java.util.Map;
public interface UserRepository {
Map<User, Boolean> linkWithRoom(Room room, Collection<User> users, boolean force);
Map<User, Boolean> linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException;
int unlinkWithRoom(Room room, Collection<User> users);

View File

@ -2,6 +2,7 @@ package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository;
@ -22,10 +23,10 @@ public class NodeRepositoryImpl implements NodeRepository {
private final Map<String, Node> nodeMap = new ConcurrentHashMap<>();
@Override
public void create(Node node) {
public void create(Node node) throws NodeAlreadyExistException {
synchronized (nodeMap) {
if (nodeMap.containsKey(node.id())) {
throw new IllegalArgumentException("Node with id '" + node.id() + "' already exists");
throw new NodeAlreadyExistException(node.id());
}
nodeMap.put(node.id(), node);

View File

@ -2,9 +2,9 @@ package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.picker.*;

View File

@ -2,6 +2,7 @@ package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
@ -21,7 +22,7 @@ public class RoomRepositoryImpl implements RoomRepository {
private final Map<Node, Rooms> node2roomsMap = new ConcurrentHashMap<>();
@Override
public void create(Room room) {
public void create(Room room) throws RoomAlreadyExistException {
var nodeId = room.getNodeId();
synchronized (node2roomsMap) {
@ -35,7 +36,7 @@ public class RoomRepositoryImpl implements RoomRepository {
var rooms = node2roomsMap.get(node.get());
if (rooms.containsKey(room.getId())) {
throw new IllegalArgumentException("Room already exists");
throw new RoomAlreadyExistException(room.getNodeId(), room.getId());
}
rooms.put(room.getId(), new RoomContainer(room, new AtomicInteger(0)));
pickerRepository.find(room.getNodeId()).add(room);

View File

@ -2,10 +2,9 @@ package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker;
@ -23,7 +22,7 @@ public class UserRepositoryImpl implements UserRepository {
private final Map<NodeRoomPath, Set<User>> roomUsers = new ConcurrentHashMap<>();
@Override
public Map<User, Boolean> linkWithRoom(Room room, Collection<User> users, boolean force) {
public Map<User, Boolean> linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException {
var result = new HashMap<User, Boolean>();
synchronized (usersMap) {
@ -39,7 +38,7 @@ public class UserRepositoryImpl implements UserRepository {
}
if (room.getSlots().getSlots() < usersSet.size() + users.size()) {
throw new Error("Room are full");
throw new RoomAreFullException(room.getNodeId(), room.getId());
}
}

View File

@ -1,8 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;

View File

@ -1,8 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
public interface RoomPicker extends Picker<Room, User> {

View File

@ -1,8 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList;

View File

@ -1,8 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository;
import java.util.Collection;

View File

@ -1,5 +1,7 @@
package ru.dragonestia.picker.service;
import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import java.util.List;
@ -7,7 +9,7 @@ import java.util.Optional;
public interface NodeService {
void create(Node node);
void create(Node node) throws InvalidNodeIdentifierException, NodeAlreadyExistException;
void remove(Node node);

View File

@ -1,5 +1,7 @@
package ru.dragonestia.picker.service;
import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
@ -9,7 +11,7 @@ import java.util.Optional;
public interface RoomService {
void create(Room room);
void create(Room room) throws InvalidRoomIdentifierException, RoomAlreadyExistException;
void remove(Room room);

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.service;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
@ -10,7 +11,7 @@ public interface UserService {
List<Room> getUserRooms(User user);
int linkUsersWithRoom(Room room, Collection<User> users, boolean force);
int linkUsersWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException;
void unlinkUsersFromRoom(Room room, Collection<User> users);

View File

@ -2,6 +2,8 @@ package ru.dragonestia.picker.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.service.NodeService;
@ -15,13 +17,11 @@ import java.util.Optional;
public class NodeServiceImpl implements NodeService {
private final NodeRepository nodeRepository;
private final NamingValidator namingValidator;
@Override
public void create(Node node) {
if (!NamingValidator.validateNodeId(node.id())) {
throw new Error("Invalid node id format");
}
public void create(Node node) throws InvalidNodeIdentifierException, NodeAlreadyExistException {
namingValidator.validateNodeId(node.id());
nodeRepository.create(node);
}

View File

@ -2,6 +2,8 @@ package ru.dragonestia.picker.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
@ -17,13 +19,11 @@ import java.util.Optional;
public class RoomServiceImpl implements RoomService {
private final RoomRepository roomRepository;
private final NamingValidator namingValidator;
@Override
public void create(Room room) {
if (!NamingValidator.validateRoomId(room.getId())) {
throw new Error("Invalid room id format");
}
public void create(Room room) throws InvalidRoomIdentifierException, RoomAlreadyExistException {
namingValidator.validateRoomId(room.getNodeId(), room.getId());
roomRepository.create(room);
}

View File

@ -1,19 +1,51 @@
package ru.dragonestia.picker.util;
import lombok.experimental.UtilityClass;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException;
import ru.dragonestia.picker.api.exception.InvalidUsernamesException;
import ru.dragonestia.picker.api.utils.ValidateIdentifier;
import ru.dragonestia.picker.model.User;
@UtilityClass
import java.util.LinkedList;
import java.util.List;
@Component
public class NamingValidator {
public boolean validateNodeId(String input) {
return input.matches("^[a-z\\d-]+$");
public void validateNodeId(String input) throws InvalidNodeIdentifierException {
if (ValidateIdentifier.forNode(input)) return;
throw new InvalidNodeIdentifierException(input);
}
public boolean validateRoomId(String input) {
return input.matches("^[a-z\\d-]+$");
public void validateRoomId(String nodeId, String input) throws InvalidRoomIdentifierException {
if (ValidateIdentifier.forRoom(input)) return;
throw new InvalidRoomIdentifierException(nodeId, input);
}
public boolean validateUserId(String input) {
return input.matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$");
private boolean validateUserId(String input) {
return ValidateIdentifier.forUser(input);
}
public List<User> validateUserIds(List<String> input) throws InvalidUsernamesException {
var users = new LinkedList<User>();
var invalid = new LinkedList<String>();
for (var username: input) {
if (validateUserId(username)) {
users.add(new User(username));
continue;
}
invalid.add(username);
}
if (!invalid.isEmpty()) {
throw new InvalidUsernamesException(input, invalid);
}
return users;
}
}

View File

@ -3,10 +3,10 @@ package ru.dragonestia.picker.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.RoomRepository;

View File

@ -23,8 +23,11 @@ ext {
}
dependencies {
implementation project(":api")
implementation 'com.vaadin:vaadin-spring-boot-starter'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

View File

@ -9,18 +9,15 @@ import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import lombok.Getter;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.model.User;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class AddUsers extends Details {
@ -79,8 +76,7 @@ public class AddUsers extends Details {
try {
onCommit.accept(readAllUsers(), ignoreSlots.getValue());
} catch (Error error) {
Notification.show(error.getMessage(), 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error(error.getMessage());
}
clear();

View File

@ -10,7 +10,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
public class NavPath extends HorizontalLayout{
public NavPath(Point root, Point... points) {
private NavPath(Point root, Point... points) {
setWidth("100%");
setAlignItems(Alignment.CENTER);
getStyle().set("background-color", "#F3F3F3")
@ -58,5 +58,20 @@ public class NavPath extends HorizontalLayout{
return button;
}
public record Point(String name, String uri) {}
public static NavPath rootNodes() {
return new NavPath(new NavPath.Point("Nodes", "/nodes"));
}
public static NavPath toNode(String nodeId) {
return new NavPath(new NavPath.Point("Nodes", "/nodes"),
new NavPath.Point(nodeId, "/nodes/" + nodeId));
}
public static NavPath toRoom(String nodeId, String roomId) {
return new NavPath(new NavPath.Point("Nodes", "/nodes"),
new NavPath.Point(nodeId, "/nodes/" + nodeId),
new NavPath.Point(roomId, "/nodes/" + nodeId + "/rooms/" + roomId));
}
private record Point(String name, String uri) {}
}

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.cp.component;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
@ -8,13 +9,11 @@ import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import lombok.Setter;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.api.model.Node;
import java.util.List;
import java.util.function.Consumer;
@ -51,14 +50,14 @@ public class NodeList extends VerticalLayout {
var temp = input.trim();
nodesGrid.setItems(cachedNodes.stream()
.filter(node -> node.id().startsWith(temp))
.filter(node -> node.getId().startsWith(temp))
.toList());
}
private Grid<Node> createGrid() {
var grid = new Grid<>(Node.class, false);
grid.addColumn(Node::id).setHeader("Identifier");
grid.addColumn(node -> node.mode().getName()).setHeader("Mode");
grid.addColumn(Node::getId).setHeader("Identifier");
grid.addColumn(node -> node.getMode().getName()).setHeader("Mode");
grid.addComponentColumn(this::createManageButtons).setHeader("Manage");
return grid;
}
@ -84,29 +83,28 @@ public class NodeList extends VerticalLayout {
}
private void clickDetailsButton(Node node) {
getUI().ifPresent(ui -> ui.navigate("/nodes/" + node.id()));
getUI().ifPresent(ui -> ui.navigate("/nodes/" + node.getId()));
}
private void clickRemoveButton(Node node) {
var dialog = new Dialog("Confirm node deletion");
dialog.add(new Paragraph("Confirm that you want to delete node. Enter '" + node.id() + "' to field below and confirm."));
dialog.add(new Html("<p>Confirm that you want to delete node. Enter <b><u>" + node.getId() + "</u></b> to field below and confirm.</p>"));
var inputField = new TextField();
inputField.setWidth("100%");
dialog.add(inputField);
{ // confirm
var button = new Button("Confirm");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> {
if (!node.id().equals(inputField.getValue())) {
Notification.show("Invalid input", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
if (!node.getId().equals(inputField.getValue())) {
Notifications.error("Invalid input");
return;
}
removeNode(node);
Notification.show("Node '" + node.id() + "' was successfully removed!", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
Notifications.success("Node <b>" + node.getId() + "</b> was successfully removed!");
dialog.close();
});
@ -129,7 +127,7 @@ public class NodeList extends VerticalLayout {
private void removeNode(Node node) {
if (removeMethod != null) {
removeMethod.accept(node.id());
removeMethod.accept(node.getId());
}
}
}

View File

@ -0,0 +1,53 @@
package ru.dragonestia.picker.cp.component;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Notifications {
public Notification success(String text) {
var notification = create(VaadinIcon.CHECK_CIRCLE, text);
notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
notification.open();
return notification;
}
public Notification warn(String text) {
var notification = create(VaadinIcon.WARNING, text);
notification.addThemeVariants(NotificationVariant.LUMO_WARNING);
notification.open();
return notification;
}
public Notification error(String text) {
var notification = create(VaadinIcon.WARNING, text);
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
notification.open();
return notification;
}
private Notification create(VaadinIcon icon, String text) {
var layout = new HorizontalLayout();
layout.add(new Icon(icon));
layout.add(new Html("<span>" + text + "</span>"));
var notification = new Notification();
notification.setDuration(5000);
notification.setPosition(Notification.Position.TOP_END);
var closeButton = new Button(VaadinIcon.CLOSE_SMALL.create(), event -> notification.close());
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
layout.add(closeButton);
notification.add(layout);
return notification;
}
}

View File

@ -7,8 +7,6 @@ import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.details.Details;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.radiobutton.RadioButtonGroup;
import com.vaadin.flow.component.radiobutton.RadioGroupVariant;
@ -16,8 +14,8 @@ import com.vaadin.flow.component.textfield.Autocomplete;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import org.springframework.lang.Nullable;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.type.PickingMode;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.model.type.PickingMode;
import java.util.function.Function;
@ -91,8 +89,7 @@ public class RegisterNode extends Details {
error = "Invalid node id format";
}
Notification.show(error, 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error(error);
return;
}
@ -100,13 +97,11 @@ public class RegisterNode extends Details {
var response = onSubmit.apply(node);
clear();
if (response.error()) {
Notification.show(response.reason(), 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error(response.reason());
return;
}
Notification.show("Node was successfully registered", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
Notifications.success("Node was successfully registered");
}
public record Response(boolean error, @Nullable String reason) {}

View File

@ -6,16 +6,13 @@ import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.details.Details;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.Autocomplete;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import org.springframework.lang.Nullable;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.type.SlotLimit;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.model.Room;
import java.util.function.Function;
@ -45,7 +42,7 @@ public class RegisterRoom extends Details {
private TextField createNodeIdentifierField() {
var field = new TextField("Node identifier");
field.setMinWidth(20, Unit.REM);
field.setValue(node.id());
field.setValue(node.getId());
field.setReadOnly(true);
return field;
}
@ -106,23 +103,20 @@ public class RegisterRoom extends Details {
error = "Invalid room id format";
}
Notification.show(error, 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error(error);
return;
}
var room = Room.create(nodeIdentifier, node, SlotLimit.unlimited(), payloadField.getValue());
var room = new Room(nodeIdentifier, node, Room.INFINITE_SLOTS, payloadField.getValue());
room.setLocked(lockedField.getValue());
var response = onSubmit.apply(room);
clear();
if (response.error()) {
Notification.show(response.reason(), 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error(response.reason());
return;
}
Notification.show("Room was successfully registered", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
Notifications.success("Room was successfully registered");
}
public record Response(boolean error, @Nullable String reason) {}

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.cp.component;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
@ -10,13 +11,11 @@ import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import lombok.Setter;
import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import ru.dragonestia.picker.api.model.Room;
import java.util.List;
import java.util.function.Consumer;
@ -24,12 +23,12 @@ import java.util.function.Consumer;
public class RoomList extends VerticalLayout {
private final String nodeIdentifier;
private final Grid<RoomDTO> roomsGrid;
private final Grid<Room.Short> roomsGrid;
private final TextField searchField;
private List<RoomDTO> cachedRooms;
@Setter private Consumer<RoomDTO> removeMethod;
private List<Room.Short> cachedRooms;
@Setter private Consumer<Room.Short> removeMethod;
public RoomList(String nodeIdentifier, List<RoomDTO> buckets) {
public RoomList(String nodeIdentifier, List<Room.Short> buckets) {
this.nodeIdentifier = nodeIdentifier;
cachedRooms = buckets;
@ -57,9 +56,9 @@ public class RoomList extends VerticalLayout {
.toList());
}
private Grid<RoomDTO> createGrid() {
var grid = new Grid<>(RoomDTO.class, false);
grid.addColumn(RoomDTO::id).setHeader("Identifier");
private Grid<Room.Short> createGrid() {
var grid = new Grid<>(Room.Short.class, false);
grid.addColumn(Room.Short::id).setHeader("Identifier");
grid.addComponentColumn(room -> {
var result = new Span();
if (room.slots() == -1) {
@ -84,7 +83,7 @@ public class RoomList extends VerticalLayout {
return grid;
}
private HorizontalLayout createManageButtons(RoomDTO room) {
private HorizontalLayout createManageButtons(Room.Short room) {
var layout = new HorizontalLayout();
{
@ -104,18 +103,19 @@ public class RoomList extends VerticalLayout {
return layout;
}
private void clickDetailsButton(RoomDTO bucket) {
private void clickDetailsButton(Room.Short bucket) {
getUI().ifPresent(ui -> {
ui.navigate("/nodes/" + nodeIdentifier +
"/rooms/" + bucket.id());
});
}
private void clickRemoveButton(RoomDTO bucket) {
private void clickRemoveButton(Room.Short bucket) {
var dialog = new Dialog("Confirm bucket deletion");
dialog.add(new Paragraph("Confirm that you want to delete bucket. Enter '" + bucket.id() + "' to field below and confirm."));
dialog.add(new Html("<p>Confirm that you want to delete bucket. Enter <b><u>" + bucket.id() + "</u></b> to field below and confirm.</p>"));
var inputField = new TextField();
inputField.setWidth("100%");
dialog.add(inputField);
{ // confirm
@ -123,14 +123,12 @@ public class RoomList extends VerticalLayout {
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> {
if (!bucket.id().equals(inputField.getValue())) {
Notification.show("Invalid input", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error("Invalid input");
return;
}
removeBucket(bucket);
Notification.show("Bucket '" + bucket.id() + "' was successfully removed!", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
Notifications.success("Bucket <b>" + bucket.id() + "</b> was successfully removed!");
dialog.close();
});
@ -146,12 +144,12 @@ public class RoomList extends VerticalLayout {
dialog.open();
}
public void update(List<RoomDTO> buckets) {
public void update(List<Room.Short> buckets) {
cachedRooms = buckets;
applySearch(searchField.getValue());
}
private void removeBucket(RoomDTO bucket) {
private void removeBucket(Room.Short bucket) {
if (removeMethod != null) {
removeMethod.accept(bucket);
}

View File

@ -4,8 +4,8 @@ import com.vaadin.flow.component.grid.ColumnTextAlign;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.model.User;
import java.util.ArrayList;
import java.util.List;
@ -28,7 +28,7 @@ public class UserList extends VerticalLayout {
private Grid<User> createUsersGrid() {
var grid = new Grid<User>();
grid.addColumn(User::id).setHeader("User Identifier").setFooter(totalUsers);
grid.addColumn(User::getId).setHeader("User Identifier").setFooter(totalUsers);
grid.addColumn(user -> 0).setTextAlign(ColumnTextAlign.CENTER).setHeader("Linked with rooms") // TODO
.setFooter(occupancy);
grid.addComponentColumn(user -> new Span("buttons")).setHeader("Manage"); // TODO
@ -39,6 +39,12 @@ public class UserList extends VerticalLayout {
cachedUsers = users;
usersGrid.setItems(users);
totalUsers.setText("Total users: " + users.size());
occupancy.setText("Occupancy: %s".formatted(room.getUsingPercentage(users.size())));
occupancy.setText("Occupancy: %s".formatted(getUsingPercentage(room, users.size())));
}
private String getUsingPercentage(Room room, int usedSlots) {
if (room.isUnlimited()) return "0%";
double percent = usedSlots / (double) room.getSlots() * 100;
return ((int) percent) + "%";
}
}

View File

@ -17,7 +17,7 @@ public class RestApiConfig {
}
@Bean
Supplier<RestTemplate> restTemplate(@Autowired RestTemplateBuilder builder) {
Supplier<RestTemplate> restTemplateSupplier(@Autowired RestTemplateBuilder builder) {
return builder::build;
}
}

View File

@ -0,0 +1,37 @@
package ru.dragonestia.picker.cp.error;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.server.Command;
import com.vaadin.flow.server.ErrorEvent;
import com.vaadin.flow.server.ErrorHandler;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.api.exception.ApiException;
import ru.dragonestia.picker.cp.component.Notifications;
@Log4j2
public class ApplicationErrorHandler implements ErrorHandler {
@Override
public void error(ErrorEvent errorEvent) {
if (UI.getCurrent() == null) {
log.throwing(errorEvent.getThrowable());
return;
}
if (errorEvent.getThrowable() instanceof ApiException ex) {
execute(() -> {
Notifications.error(ex.getMessage());
});
return;
}
execute(() -> {
Notifications.error("Internal server error");
});
log.throwing(errorEvent.getThrowable());
}
private void execute(Command command) {
UI.getCurrent().access(command);
}
}

View File

@ -0,0 +1,19 @@
package ru.dragonestia.picker.cp.listener;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.cp.error.ApplicationErrorHandler;
@Log4j2
@SpringComponent
public class VaadinEventListener implements VaadinServiceInitListener {
@Override
public void serviceInit(ServiceInitEvent event) {
event.getSource().addSessionInitListener(e -> {
e.getSession().setErrorHandler(new ApplicationErrorHandler());
});
}
}

View File

@ -1,24 +0,0 @@
package ru.dragonestia.picker.cp.model;
import lombok.NonNull;
import ru.dragonestia.picker.cp.model.type.PickingMode;
import java.io.Serializable;
public record Node(@NonNull String id, @NonNull PickingMode mode) implements Serializable {
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object object) {
if (object == this) return true;
if (object == null) return false;
if (object instanceof Node other) {
return id.equals(other.id);
}
return false;
}
}

View File

@ -1,71 +0,0 @@
package ru.dragonestia.picker.cp.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import ru.dragonestia.picker.cp.model.type.SlotLimit;
import java.net.URI;
@Getter
public class Room {
private final String id;
private final String nodeId;
private final SlotLimit slots;
private final String payload;
private boolean locked = false;
@JsonCreator
private Room(@JsonProperty("id") String id,
@JsonProperty("nodeIdentifier") String nodeId,
@JsonProperty("slots") SlotLimit slots,
@JsonProperty("payload") String payload,
@JsonProperty("locked") boolean locked) {
this.id = id;
this.nodeId = nodeId;
this.slots = slots;
this.payload = payload;
this.locked = locked;
}
public static Room create(String roomId, Node node, SlotLimit limit, String payload) {
return new Room(roomId, node.id(), limit, payload, false);
}
public void setLocked(boolean value) {
locked = value;
}
public boolean isAvailable(int usedSlots, int requiredSlots) {
if (locked) return false;
if (slots.isUnlimited()) return true;
return slots.slots() >= usedSlots + requiredSlots;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object object) {
if (object == this) return true;
if (object == null) return false;
if (object instanceof Room other) {
return id.equals(other.id);
}
return false;
}
public URI createApiURI() {
return URI.create("/nodes/" + nodeId + "/rooms/" + id);
}
public String getUsingPercentage(int used) {
if (getSlots().isUnlimited()) return "0%";
double percent = used / (double) getSlots().slots() * 100;
return ((int) percent) + "%";
}
}

View File

@ -1,16 +0,0 @@
package ru.dragonestia.picker.cp.model.dto;
import ru.dragonestia.picker.cp.model.Node;
import java.net.URI;
public record RoomDTO(String id, int slots, boolean locked) {
public URI uriAPI(Node node) {
return uriAPI(node.id());
}
public URI uriAPI(String nodeId) {
return URI.create("/nodes/" + nodeId + "/rooms/" + id);
}
}

View File

@ -1,21 +0,0 @@
package ru.dragonestia.picker.cp.model.type;
import java.beans.Transient;
public record SlotLimit(int slots) {
private final static int UNLIMITED_VALUE = -1;
public static SlotLimit unlimited() {
return new SlotLimit(UNLIMITED_VALUE);
}
public static SlotLimit of(int slots) {
return new SlotLimit(slots);
}
@Transient
public boolean isUnlimited() {
return slots == UNLIMITED_VALUE;
}
}

View File

@ -3,10 +3,11 @@ package ru.dragonestia.picker.cp.page;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.*;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
@Route("/")
public class HomePage extends VerticalLayout {
public class HomePage extends VerticalLayout implements BeforeEnterObserver {
public HomePage() {
super();
@ -14,4 +15,9 @@ public class HomePage extends VerticalLayout {
add(new H1("Hello world!"));
add(new Paragraph("Hello world!"));
}
@Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
throw new NodeNotFoundException("gdfsg");
}
}

View File

@ -4,72 +4,54 @@ import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Hr;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.repository.NodeRepository;
import ru.dragonestia.picker.api.repository.RoomRepository;
import ru.dragonestia.picker.cp.component.Notifications;
import ru.dragonestia.picker.cp.component.RoomList;
import ru.dragonestia.picker.cp.component.NavPath;
import ru.dragonestia.picker.cp.component.RegisterRoom;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import ru.dragonestia.picker.cp.repository.RoomRepository;
import ru.dragonestia.picker.cp.repository.NodeRepository;
import ru.dragonestia.picker.cp.util.RouteParamsExtractor;
import java.util.List;
@Getter
@RequiredArgsConstructor
@PageTitle("Rooms")
@Route("/nodes/:nodeId")
public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserver {
private final NodeRepository nodeRepository;
private final RoomRepository roomRepository;
private final RouteParamsExtractor paramsExtractor;
private Node node;
private RegisterRoom registerRoom;
private RoomList roomList;
public NodeDetailsPage(@Autowired NodeRepository nodeRepository,
@Autowired RoomRepository roomRepository) {
this.nodeRepository = nodeRepository;
this.roomRepository = roomRepository;
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
var nodeIdOpt = event.getRouteParameters().get("nodeId");
if (nodeIdOpt.isEmpty()) {
getUI().ifPresent(ui -> ui.navigate("/nodes"));
return;
}
var nodeId = nodeIdOpt.get();
add(new NavPath(new NavPath.Point("Nodes", "/nodes"), new NavPath.Point(nodeId, "/nodes/" + nodeId)));
var nodeOpt = nodeRepository.find(nodeId);
if (nodeOpt.isEmpty()) {
add(new H2("Error 404"));
add(new Paragraph("Node not found"));
Notification.show("Node '" + nodeId + "' does not exist", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
node = nodeOpt.get();
node = paramsExtractor.extractNodeId(event);
initComponents(node, roomRepository.all(node));
}
private void initComponents(Node node, List<RoomDTO> rooms) {
private void initComponents(Node node, List<Room.Short> rooms) {
add(NavPath.toNode(node.getId()));
printNodeDetails(node);
add(new Hr());
add(registerRoom = new RegisterRoom(node, (bucket) -> {
add(registerRoom = new RegisterRoom(node, (room) -> {
try {
roomRepository.register(bucket);
roomRepository.register(room);
return new RegisterRoom.Response(false, null);
} catch (Error error) {
return new RegisterRoom.Response(true, error.getMessage());
@ -78,9 +60,9 @@ public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserv
}
}));
add(new Hr());
add(roomList = new RoomList(node.id(), rooms));
roomList.setRemoveMethod(bucket -> {
roomRepository.remove(node, bucket);
add(roomList = new RoomList(node.getId(), rooms));
roomList.setRemoveMethod(room -> {
roomRepository.remove(node, room);
roomList.update(roomRepository.all(node));
});
}
@ -89,8 +71,8 @@ public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserv
add(new H2("Node details"));
var layout = new VerticalLayout();
layout.add(new Html("<span>Identifier: <b>" + node.id() + "</b></span>"));
layout.add(new Html("<span>Mode: <b>" + node.mode().getName() + "</b></span>"));
layout.add(new Html("<span>Identifier: <b>" + node.getId() + "</b></span>"));
layout.add(new Html("<span>Mode: <b>" + node.getMode().getName() + "</b></span>"));
add(layout);
}

View File

@ -7,10 +7,11 @@ import com.vaadin.flow.router.Route;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import ru.dragonestia.picker.api.exception.ApiException;
import ru.dragonestia.picker.api.repository.NodeRepository;
import ru.dragonestia.picker.cp.component.NavPath;
import ru.dragonestia.picker.cp.component.NodeList;
import ru.dragonestia.picker.cp.component.RegisterNode;
import ru.dragonestia.picker.cp.repository.NodeRepository;
@Log4j2
@Getter
@ -26,7 +27,7 @@ public class NodesPage extends VerticalLayout {
super();
this.nodeRepository = nodeRepository;
add(new NavPath(new NavPath.Point("Nodes", "/nodes")));
add(NavPath.rootNodes());
add(registerNode = createRegisterNodeElement());
add(new Hr());
add(nodeList = createNodeListElement());
@ -41,10 +42,7 @@ public class NodesPage extends VerticalLayout {
try {
nodeRepository.register(node);
return new RegisterNode.Response(false, "");
} catch (Error ex) {
return new RegisterNode.Response(true, ex.getMessage());
} catch (RuntimeException ex) {
log.throwing(ex);
} catch (ApiException ex) {
return new RegisterNode.Response(true, ex.getMessage());
} finally {
nodeList.update(nodeRepository.all());

View File

@ -8,28 +8,30 @@ import com.vaadin.flow.component.html.Hr;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.model.User;
import ru.dragonestia.picker.api.repository.NodeRepository;
import ru.dragonestia.picker.api.repository.RoomRepository;
import ru.dragonestia.picker.api.repository.UserRepository;
import ru.dragonestia.picker.cp.component.AddUsers;
import ru.dragonestia.picker.cp.component.NavPath;
import ru.dragonestia.picker.cp.component.Notifications;
import ru.dragonestia.picker.cp.component.UserList;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.User;
import ru.dragonestia.picker.cp.repository.RoomRepository;
import ru.dragonestia.picker.cp.repository.NodeRepository;
import ru.dragonestia.picker.cp.repository.UserRepository;
import ru.dragonestia.picker.cp.util.RouteParamsExtractor;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;
@RequiredArgsConstructor
@PageTitle("Room details")
@Route("/nodes/:nodeId/rooms/:roomId")
public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserver {
@ -37,6 +39,8 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
private final NodeRepository nodeRepository;
private final RoomRepository roomRepository;
private final UserRepository userRepository;
private final RouteParamsExtractor paramsExtractor;
private Node node;
private Room room;
private AddUsers addUsers;
@ -44,57 +48,16 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
private Button lockRoomButton;
private VerticalLayout roomInfo;
@Autowired
public RoomDetailsPage(NodeRepository nodeRepository, RoomRepository roomRepository, UserRepository userRepository) {
this.nodeRepository = nodeRepository;
this.roomRepository = roomRepository;
this.userRepository = userRepository;
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
var nodeIdOpt = event.getRouteParameters().get("nodeId");
if (nodeIdOpt.isEmpty()) {
getUI().ifPresent(ui -> ui.navigate("/nodes"));
return;
}
var roomIdOpt = event.getRouteParameters().get("roomId");
if (roomIdOpt.isEmpty()) {
getUI().ifPresent(ui -> ui.navigate("/rooms/" + nodeIdOpt.get()));
return;
}
var nodeId = nodeIdOpt.get();
var roomId = roomIdOpt.get();
add(new NavPath(new NavPath.Point("Nodes", "/nodes"),
new NavPath.Point(nodeId, "/nodes/" + nodeId),
new NavPath.Point(roomId, "/nodes/" + nodeId + "/rooms/" + roomId)));
var nodeOpt = nodeRepository.find(nodeId);
if (nodeOpt.isEmpty()) {
add(new H2("Error 404"));
add(new Paragraph("Node not found!"));
Notification.show("Node '" + nodeId + "' does not exist", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
node = nodeOpt.get();
var bucketOpt = roomRepository.find(node, roomId);
if (bucketOpt.isEmpty()) {
add(new H2("Error 404"));
add(new Paragraph("Room not found!"));
Notification.show("Room '" + nodeId + "' does not exist", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
room = bucketOpt.get();
node = paramsExtractor.extractNodeId(event);
room = paramsExtractor.extractRoomId(event, node);
init();
}
private void init() {
add(NavPath.toRoom(node.getId(), room.getId()));
add(new H2("Room details"));
printRoomDetails();
add(new Hr());
@ -108,7 +71,7 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
roomInfo.removeAll();
roomInfo.add(new Html("<span>Node identifier: <b>" + room.getNodeId() + "</b></span>"));
roomInfo.add(new Html("<span>Room identifier: <b>" + room.getId() + "</b></span>"));
roomInfo.add(new Html("<span>Slots: <b>" + (room.getSlots().isUnlimited()? "Unlimited" : room.getSlots().slots()) + "</b></span>"));
roomInfo.add(new Html("<span>Slots: <b>" + (room.isUnlimited()? "Unlimited" : room.getSlots()) + "</b></span>"));
roomInfo.add(new Html("<span>Locked: <b>" + (room.isLocked()? "Yes" : "No") + "</b></span>"));
}
@ -139,20 +102,13 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
private void changeBucketLockedState() {
var newValue = !room.isLocked();
try {
roomRepository.lock(room, newValue);
} catch (Error error) {
Notification.show(error.getMessage(), 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
roomRepository.lock(room, newValue);
room.setLocked(newValue);
setLockRoomButtonState();
updateRoomInfo();
Notification.show("Success", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
Notifications.success("Success");
}
private void appendUsers(Room room, Collection<User> users, boolean ignoreLimitation) {
@ -160,7 +116,7 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
var newUsers = users.stream()
.filter(user -> {
if (user.id().matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$")) {
if (user.getId().matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$")) {
return true;
}
@ -173,15 +129,12 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
if (validationFail.get()) {
if (newUsers.isEmpty()) {
Notification.show("All users entered were added because they do not comply with the rule for writing the user identifier", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
Notifications.error("All users entered were added because they do not comply with the rule for writing the user identifier");
} else {
Notification.show("Not all users entered were added because they do not comply with the rule for writing the user identifier", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_WARNING);
Notifications.warn("Not all users entered were added because they do not comply with the rule for writing the user identifier");
}
} else {
Notification.show("Success", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
Notifications.success("Success");
}
}
}

View File

@ -0,0 +1,15 @@
package ru.dragonestia.picker.cp.page.plug;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import ru.dragonestia.picker.cp.component.NavPath;
public abstract class ErrorPlug extends VerticalLayout {
public void init(NavPath path, String title, String description) {
add(path);
add(new H1(title));
add(new Html("<p>" + description + "</p>"));
}
}

View File

@ -0,0 +1,20 @@
package ru.dragonestia.picker.cp.page.plug;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.ErrorParameter;
import com.vaadin.flow.router.HasErrorParameter;
import jakarta.servlet.http.HttpServletResponse;
import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.cp.component.NavPath;
public class InvalidNodeIdentifierPlug extends ErrorPlug implements HasErrorParameter<InvalidNodeIdentifierException> {
@Override
public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter<InvalidNodeIdentifierException> errorParameter) {
var ex = errorParameter.getException();
var nodeId = ex.getNodeId();
init(NavPath.toNode(nodeId), "Error 400", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -0,0 +1,21 @@
package ru.dragonestia.picker.cp.page.plug;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.ErrorParameter;
import com.vaadin.flow.router.HasErrorParameter;
import jakarta.servlet.http.HttpServletResponse;
import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException;
import ru.dragonestia.picker.cp.component.NavPath;
public class InvalidRoomIdentifierPlug extends ErrorPlug implements HasErrorParameter<InvalidRoomIdentifierException> {
@Override
public int setErrorParameter(BeforeEnterEvent event, ErrorParameter<InvalidRoomIdentifierException> errorParameter) {
var ex = errorParameter.getException();
var nodeId = ex.getNodeId();
var roomId = ex.getRoomId();
init(NavPath.toRoom(nodeId, roomId), "Error 400", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -0,0 +1,20 @@
package ru.dragonestia.picker.cp.page.plug;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.ErrorParameter;
import com.vaadin.flow.router.HasErrorParameter;
import jakarta.servlet.http.HttpServletResponse;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.cp.component.NavPath;
public class NodeNotFoundPlug extends ErrorPlug implements HasErrorParameter<NodeNotFoundException> {
@Override
public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter<NodeNotFoundException> errorParameter) {
var ex = errorParameter.getException();
var nodeId = ex.getNodeId();
init(NavPath.toNode(nodeId), "Error 404", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -0,0 +1,21 @@
package ru.dragonestia.picker.cp.page.plug;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.ErrorParameter;
import com.vaadin.flow.router.HasErrorParameter;
import jakarta.servlet.http.HttpServletResponse;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.cp.component.NavPath;
public class RoomNotFoundPlug extends ErrorPlug implements HasErrorParameter<RoomNotFoundException> {
@Override
public int setErrorParameter(BeforeEnterEvent beforeEnterEvent, ErrorParameter<RoomNotFoundException> errorParameter) {
var ex = errorParameter.getException();
var nodeId = ex.getNodeId();
var roomId = ex.getRoomId();
init(NavPath.toRoom(nodeId, roomId), "Error 404", ex.getMessage());
return HttpServletResponse.SC_NOT_FOUND;
}
}

View File

@ -1,17 +0,0 @@
package ru.dragonestia.picker.cp.repository;
import ru.dragonestia.picker.cp.model.Node;
import java.util.List;
import java.util.Optional;
public interface NodeRepository {
void register(Node node);
List<Node> all();
Optional<Node> find(String nodeId);
void remove(String nodeId);
}

View File

@ -1,23 +0,0 @@
package ru.dragonestia.picker.cp.repository;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import java.util.List;
import java.util.Optional;
public interface RoomRepository {
List<RoomDTO> all(Node node);
void register(Room room);
void remove(Room room);
void remove(Node node, RoomDTO bucket);
Optional<Room> find(Node node, String roomId);
void lock(Room room, boolean value);
}

View File

@ -1,16 +0,0 @@
package ru.dragonestia.picker.cp.repository;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User;
import java.util.Collection;
import java.util.List;
public interface UserRepository {
void linkWithRoom(Room room, Collection<User> users, boolean force);
void unlinkFromRoom(Room room, Collection<User> users);
List<User> all(Room room);
}

View File

@ -3,13 +3,15 @@ package ru.dragonestia.picker.cp.repository.impl;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.repository.NodeRepository;
import ru.dragonestia.picker.cp.repository.impl.response.NodeDetailsResponse;
import ru.dragonestia.picker.cp.repository.impl.response.NodeListResponse;
import ru.dragonestia.picker.cp.repository.impl.response.NodeRegisterResponse;
import org.springframework.http.HttpMethod;
import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.repository.response.NodeDetailsResponse;
import ru.dragonestia.picker.api.repository.response.NodeListResponse;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.repository.NodeRepository;
import java.net.URI;
import java.util.List;
import java.util.Optional;
@ -21,41 +23,30 @@ public class NodeRepositoryImpl implements NodeRepository {
private final RestUtil rest;
@Override
public void register(Node node) {
NodeRegisterResponse response;
try {
response = rest.post(URI.create("nodes"),
NodeRegisterResponse.class,
params -> {
params.put("nodeId", node.id());
params.put("method", node.mode().name());
});
} catch (Exception ex) {
throw new RuntimeException("Internal error", ex);
}
if (!response.success()) {
throw new Error(response.message());
}
public void register(Node node) throws InvalidNodeIdentifierException, NodeAlreadyExistException {
rest.query("nodes", HttpMethod.POST, params -> {
params.put("nodeId", node.getId());
params.put("method", node.getMode().name());
});
}
@Override
public List<Node> all() {
return rest.get(URI.create("nodes"), NodeListResponse.class).nodes();
return rest.query("nodes", HttpMethod.GET, NodeListResponse.class, params -> {}).nodes();
}
@Override
public Optional<Node> find(String nodeId) {
try {
var response = rest.get(URI.create("nodes/" + nodeId), NodeDetailsResponse.class);
var response = rest.query("nodes/" + nodeId, HttpMethod.GET, NodeDetailsResponse.class, params -> {});
return Optional.of(response.node());
} catch (Exception ex) {
} catch (NodeNotFoundException ex) {
return Optional.empty();
}
}
@Override
public void remove(String nodeId) {
rest.delete(URI.create("nodes/" + nodeId), params -> {});
rest.query("nodes/" + nodeId, HttpMethod.DELETE, params -> {});
}
}

View File

@ -1,15 +1,15 @@
package ru.dragonestia.picker.cp.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import ru.dragonestia.picker.api.exception.ExceptionFactory;
import ru.dragonestia.picker.api.repository.response.ErrorResponse;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -18,67 +18,41 @@ import java.util.function.Supplier;
public class RestUtil {
private final URI serverUrl;
private final Supplier<RestTemplate> restTemplate;
private final Supplier<RestTemplate> restTemplateSupplier;
public <T> T get(URI uri, Class<T> responseType) {
var template = restTemplate.get();
return Objects.requireNonNull(template.getForObject(serverUrl.resolve(uri), responseType));
public void query(String uri, HttpMethod method) {
query(uri, method, ParamsConsumer.NONE);
}
public <T> ResponseEntity<T> getEntity(URI uri, Class<T> responseType) {
var template = restTemplate.get();
return template.getForEntity(serverUrl.resolve(uri), responseType);
}
public <T> T get(URI uri, Class<T> responseType, Consumer<Map<String, String>> paramsConsumer) {
public void query(String uri, HttpMethod method, ParamsConsumer paramsConsumer) {
var params = new HashMap<String, String>();
paramsConsumer.accept(params);
var template = restTemplate.get();
return Objects.requireNonNull(template.getForObject(buildPath(uri, params.keySet()),
responseType,
params));
var template = restTemplateSupplier.get();
try {
template.exchange(buildPath(uri, params.keySet()), method, null, String.class, params);
} catch (HttpClientErrorException ex) {
throw ExceptionFactory.of(Objects.requireNonNull(ex.getResponseBodyAs(ErrorResponse.class)));
}
}
public <T> T post(URI uri, Class<T> responseType, Consumer<Map<String, String>> paramsConsumer) {
public <T> T query(String uri, HttpMethod method, Class<T> clazz) {
return query(uri, method, clazz, ParamsConsumer.NONE);
}
public <T> T query(String uri, HttpMethod method, Class<T> clazz, ParamsConsumer paramsConsumer) {
var params = new HashMap<String, String>();
paramsConsumer.accept(params);
var template = restTemplate.get();
return Objects.requireNonNull(template.postForObject(buildPath(uri, params.keySet()),
null,
responseType,
params));
var template = restTemplateSupplier.get();
try {
return template.exchange(buildPath(uri, params.keySet()), method, null, clazz, params).getBody();
} catch (HttpClientErrorException ex) {
throw ExceptionFactory.of(Objects.requireNonNull(ex.getResponseBodyAs(ErrorResponse.class)));
}
}
public <T> ResponseEntity<T> postEntity(URI uri, Class<T> responseType, Consumer<Map<String, String>> paramsConsumer) {
var params = new HashMap<String, String>();
paramsConsumer.accept(params);
var template = restTemplate.get();
return template.postForEntity(buildPath(uri, params.keySet()),
null,
responseType,
params);
}
public void put(URI uri, Consumer<Map<String, String>> paramsConsumer) {
var params = new HashMap<String, String>();
paramsConsumer.accept(params);
var template = restTemplate.get();
template.put(buildPath(uri, params.keySet()), params);
}
public void delete(URI uri, Consumer<Map<String, String>> paramsConsumer) {
var params = new HashMap<String, String>();
paramsConsumer.accept(params);
var template = restTemplate.get();
template.delete(buildPath(uri, params.keySet()), params);
}
private String buildPath(URI uri, Collection<String> paramKeys) {
private String buildPath(String uri, Collection<String> paramKeys) {
var path = new StringBuilder(serverUrl.resolve(uri) + "?");
int left = paramKeys.size();
for (var key: paramKeys) {
@ -92,4 +66,9 @@ public class RestUtil {
}
return path.toString();
}
public interface ParamsConsumer extends Consumer<Map<String, String>> {
ParamsConsumer NONE = map -> {};
}
}

View File

@ -3,18 +3,18 @@ package ru.dragonestia.picker.cp.repository.impl;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.client.HttpClientErrorException;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import ru.dragonestia.picker.cp.repository.RoomRepository;
import ru.dragonestia.picker.cp.repository.impl.response.RoomInfoResponse;
import ru.dragonestia.picker.cp.repository.impl.response.RoomListResponse;
import ru.dragonestia.picker.cp.repository.impl.response.RoomRegisterResponse;
import org.springframework.http.HttpMethod;
import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.repository.RoomRepository;
import ru.dragonestia.picker.api.repository.response.RoomInfoResponse;
import ru.dragonestia.picker.api.repository.response.RoomListResponse;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Log4j2
@ -25,76 +25,44 @@ public class RoomRepositoryImpl implements RoomRepository {
private final RestUtil rest;
@Override
public List<RoomDTO> all(Node node) {
var entity = rest.getEntity(URI.create("/nodes/" + node.id() + "/rooms"),
RoomListResponse.class);
if (entity.getStatusCode().value() == 404) {
throw new Error("Node with identifier '" + node.id() + "' does not exists'");
}
if (!entity.hasBody()) {
throw new Error("Room list did not present");
}
return Objects.requireNonNull(entity.getBody()).rooms();
public void register(Room room) throws NodeNotFoundException, InvalidRoomIdentifierException, RoomAlreadyExistException {
rest.query("/nodes/" + room.getNodeId() + "/rooms", HttpMethod.POST, params -> {
params.put("roomId", room.getId());
params.put("slots", Integer.toString(room.getSlots()));
params.put("payload", room.getPayload());
params.put("locked", Boolean.toString(room.isLocked()));
});
}
@Override
public void register(Room room) {
public void remove(Room room) throws NodeNotFoundException {
rest.query("/nodes/" + room.getNodeId() + "/rooms/" + room.getId(), HttpMethod.DELETE, params -> {});
}
@Override
public void remove(Node node, Room.Short room) throws NodeNotFoundException {
rest.query("/nodes/" + node.getId() + "/rooms/" + room.id(), HttpMethod.DELETE, params -> {});
}
@Override
public List<Room.Short> all(Node node) throws NodeNotFoundException {
return rest.query("/nodes/" + node.getId() + "/rooms", HttpMethod.GET, RoomListResponse.class, params -> {}).rooms();
}
@Override
public Optional<Room> find(Node node, String roomId) throws NodeNotFoundException {
try {
var response = rest.post(URI.create("/nodes/" + room.getNodeId() + "/rooms"),
RoomRegisterResponse.class,
params -> {
params.put("roomId", room.getId());
params.put("slots", Integer.toString(room.getSlots().slots()));
params.put("payload", room.getPayload());
params.put("locked", Boolean.toString(room.isLocked()));
});
if (response.success()) return;
throw new Error(response.message());
} catch (HttpClientErrorException ex) {
var response = ex.getResponseBodyAs(RoomRegisterResponse.class);
if (response != null) {
throw new Error(response.message());
}
log.throwing(ex);
throw new Error("Internal error. Check logs");
}
}
@Override
public void remove(Room room) {
rest.delete(URI.create("/nodes/" + room.getNodeId() + "/rooms/" + room.getId()), params -> {});
}
@Override
public void remove(Node node, RoomDTO room) {
rest.delete(URI.create("/nodes/" + node.id() + "/rooms/" + room.id()), params -> {});
}
@Override
public Optional<Room> find(Node node, String roomId) {
try {
var response = rest.get(URI.create("/nodes/" + node.id() + "/rooms/" + roomId), RoomInfoResponse.class, map -> {});
var response = rest.query("/nodes/" + node.getId() + "/rooms/" + roomId, HttpMethod.GET, RoomInfoResponse.class, map -> {});
return Optional.of(response.room());
} catch (Exception ex) {
} catch (RoomNotFoundException ex) {
return Optional.empty();
}
}
@Override
public void lock(Room room, boolean value) {
try {
rest.post(URI.create(room.createApiURI() + "/lock"), Boolean.class, params -> {
params.put("newState", Boolean.toString(value));
});
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Error when changing room locked state");
}
public void lock(Room room, boolean value) throws NodeNotFoundException, RoomNotFoundException {
rest.query("/nodes/%s/rooms/%s/lock".formatted(room.getNodeId(), room.getId()), HttpMethod.PUT, params -> {
params.put("newState", Boolean.toString(value));
});
}
}

View File

@ -3,13 +3,15 @@ package ru.dragonestia.picker.cp.repository.impl;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User;
import ru.dragonestia.picker.cp.repository.UserRepository;
import ru.dragonestia.picker.cp.repository.impl.response.LinkUsersWithRoomResponse;
import ru.dragonestia.picker.cp.repository.impl.response.RoomUserListResponse;
import org.springframework.http.HttpMethod;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.api.model.User;
import ru.dragonestia.picker.api.repository.UserRepository;
import ru.dragonestia.picker.api.repository.response.RoomUserListResponse;
import java.net.URI;
import java.util.Collection;
import java.util.List;
@ -21,47 +23,29 @@ public class UserRepositoryImpl implements UserRepository {
private final RestUtil rest;
@Override
public void linkWithRoom(Room room, Collection<User> users, boolean force) {
try {
var response = rest.post(URI.create("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId())),
LinkUsersWithRoomResponse.class,
params -> {
params.put("userIds", String.join(",", users.stream().map(User::id).toList()));
params.put("force", Boolean.toString(force));
}
);
if (!response.success()) {
throw new Error(response.message());
}
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Internal error");
}
public void linkWithRoom(Room room, Collection<User> users, boolean force) throws NodeNotFoundException, RoomNotFoundException, RoomAreFullException {
rest.query("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId()),
HttpMethod.POST,
params -> {
params.put("userIds", String.join(",", users.stream().map(User::getId).toList()));
params.put("force", Boolean.toString(force));
});
}
@Override
public void unlinkFromRoom(Room room, Collection<User> users) {
try {
rest.delete(URI.create("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId())),
params -> params.put("userIds", String.join(",", users.stream().map(User::id).toList())));
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Internal error");
}
public void unlinkFromRoom(Room room, Collection<User> users) throws NodeNotFoundException, RoomNotFoundException {
rest.query("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId()),
HttpMethod.DELETE,
params -> {
params.put("userIds", String.join(",", users.stream().map(User::getId).toList()));
});
}
@Override
public List<User> all(Room room) {
try {
var response = rest.get(URI.create("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId())),
RoomUserListResponse.class,
params -> {});
return response.users();
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Internal error");
}
public List<User> all(Room room) throws NodeNotFoundException, RoomNotFoundException {
return rest.query("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId()),
HttpMethod.GET,
RoomUserListResponse.class,
params -> {}).users();
}
}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl.response;
public record LinkUsersWithRoomResponse(boolean success, String message, int usedSlots, int totalSlots) {}

View File

@ -1,5 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl.response;
import ru.dragonestia.picker.cp.model.Node;
public record NodeDetailsResponse(Node node) {}

View File

@ -1,7 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl.response;
import ru.dragonestia.picker.cp.model.Node;
import java.util.List;
public record NodeListResponse(List<Node> nodes) {}

View File

@ -1,3 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl.response;
public record NodeRegisterResponse(boolean success, String message) {}

View File

@ -1,5 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl.response;
import ru.dragonestia.picker.cp.model.Room;
public record RoomInfoResponse(Room room) {}

View File

@ -1,7 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl.response;
import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import java.util.List;
public record RoomListResponse(String node, List<RoomDTO> rooms) {}

Some files were not shown because too many files have changed in this diff Show More