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 @Override
public int hashCode() { 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 { public enum PickingMode {
SEQUENTIAL_FILLING("Sequential filling"), SEQUENTIAL_FILLING("Sequential filling"),
ROUND_ROBIN("Round Robin"), ROUND_ROBIN("Round Robin"),
LEAST_PICKED("Least Picked"); LEAST_PICKED("Least Picked");
private final String name; 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; 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 { dependencies {
implementation project(":api")
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web' 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.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 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.interceptor.DebugInterceptor;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.model.type.SlotLimit; import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository; 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; package ru.dragonestia.picker.controller;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.NodeDetailsResponse; import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.controller.response.NodeListResponse; import ru.dragonestia.picker.api.model.type.PickingMode;
import ru.dragonestia.picker.controller.response.NodeRegisterResponse; 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.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.NodeService;
import ru.dragonestia.picker.service.RoomService; import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.util.NamingValidator; import ru.dragonestia.picker.util.NamingValidator;
import java.util.LinkedList; import java.util.Arrays;
import java.util.Optional;
@RestController @RestController
@RequestMapping("/nodes") @RequestMapping("/nodes")
@ -25,47 +21,35 @@ public class NodeController {
private final NodeService nodeService; private final NodeService nodeService;
private final RoomService roomService; private final RoomService roomService;
private final NamingValidator namingValidator;
@GetMapping @GetMapping
NodeListResponse allNodes() { NodeListResponse allNodes() {
return new NodeListResponse(nodeService.all()); return new NodeListResponse(nodeService.all().stream().map(Node::toResponseObject).toList());
} }
@PostMapping @PostMapping
NodeRegisterResponse registerNode(@RequestParam(name = "nodeId") String nodeId, ResponseEntity<?> registerNode(@RequestParam(name = "nodeId") String nodeId,
@RequestParam(name = "method") PickingMode method) { @RequestParam(name = "method") PickingMode method) {
try { nodeService.create(new Node(nodeId, method));
nodeService.create(new Node(nodeId, method)); return ResponseEntity.ok().build();
} catch (IllegalArgumentException ex) {
return new NodeRegisterResponse(false, ex.getMessage());
} catch (Error error) {
new NodeRegisterResponse(false, error.getMessage());
}
return new NodeRegisterResponse(true, "");
} }
@GetMapping("/{nodeId}") @GetMapping("/{nodeId}")
ResponseEntity<NodeDetailsResponse> nodeDetails(@PathVariable("nodeId") String nodeId) { ResponseEntity<NodeDetailsResponse> nodeDetails(@PathVariable("nodeId") String nodeId) {
if (!NamingValidator.validateNodeId(nodeId)) { namingValidator.validateNodeId(nodeId);
return new ResponseEntity<>(HttpStatusCode.valueOf(404));
}
var nodeOpt = nodeService.find(nodeId); return nodeService.find(nodeId)
return nodeOpt.map(node -> ResponseEntity.ok(new NodeDetailsResponse(node))) .map(node -> ResponseEntity.ok(new NodeDetailsResponse(node.toResponseObject())))
.orElseGet(() -> new ResponseEntity<>(HttpStatusCode.valueOf(404))); .orElseThrow(() -> new NodeNotFoundException(nodeId));
} }
@DeleteMapping("/{nodeId}") @DeleteMapping("/{nodeId}")
ResponseEntity<?> removeNode(@PathVariable("nodeId") String nodeId) { ResponseEntity<?> removeNode(@PathVariable("nodeId") String nodeId) {
if (!NamingValidator.validateNodeId(nodeId)) { namingValidator.validateNodeId(nodeId);
return ResponseEntity.ok().build();
}
var nodeOpt = nodeService.find(nodeId);
nodeOpt.ifPresent(nodeService::remove);
nodeService.find(nodeId).ifPresent(nodeService::remove);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@ -73,30 +57,11 @@ public class NodeController {
ResponseEntity<?> pickRoom(@PathVariable("nodeId") String nodeId, ResponseEntity<?> pickRoom(@PathVariable("nodeId") String nodeId,
@RequestParam(name = "userIds") String userIds) { @RequestParam(name = "userIds") String userIds) {
if (!NamingValidator.validateNodeId(nodeId)) { namingValidator.validateNodeId(nodeId);
return new ResponseEntity<>(HttpStatusCode.valueOf(404));
}
var nodeOpt = nodeService.find(nodeId); var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
if (nodeOpt.isEmpty()) { var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
return new ResponseEntity<>(HttpStatusCode.valueOf(404)); var room = roomService.pickAvailable(node, users);
}
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));
}
return ResponseEntity.ok(room); // TODO: make other json schema return ResponseEntity.ok(room); // TODO: make other json schema
} }

View File

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

View File

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

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; package ru.dragonestia.picker.model;
import lombok.NonNull; 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) { public record Node(@NonNull String id, @NonNull PickingMode mode) {
@ -19,4 +19,8 @@ public record Node(@NonNull String id, @NonNull PickingMode mode) {
} }
return false; 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; 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; 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; package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import java.util.List; import java.util.List;
@ -7,7 +8,7 @@ import java.util.Optional;
public interface NodeRepository { public interface NodeRepository {
void create(Node node); void create(Node node) throws NodeAlreadyExistException;
void delete(Node node); void delete(Node node);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,9 @@ package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; 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.UserRepository;
import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache; import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker; 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<>(); private final Map<NodeRoomPath, Set<User>> roomUsers = new ConcurrentHashMap<>();
@Override @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>(); var result = new HashMap<User, Boolean>();
synchronized (usersMap) { synchronized (usersMap) {
@ -39,7 +38,7 @@ public class UserRepositoryImpl implements UserRepository {
} }
if (room.getSlots().getSlots() < usersSet.size() + users.size()) { 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; 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.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap; import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;

View File

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

View File

@ -1,8 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker; 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.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList; import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList;

View File

@ -1,8 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker; 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.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import java.util.Collection; import java.util.Collection;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,51 @@
package ru.dragonestia.picker.util; 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 class NamingValidator {
public boolean validateNodeId(String input) { public void validateNodeId(String input) throws InvalidNodeIdentifierException {
return input.matches("^[a-z\\d-]+$"); if (ValidateIdentifier.forNode(input)) return;
throw new InvalidNodeIdentifierException(input);
} }
public boolean validateRoomId(String input) { public void validateRoomId(String nodeId, String input) throws InvalidRoomIdentifierException {
return input.matches("^[a-z\\d-]+$"); if (ValidateIdentifier.forRoom(input)) return;
throw new InvalidRoomIdentifierException(nodeId, input);
} }
public boolean validateUserId(String input) { private boolean validateUserId(String input) {
return input.matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$"); 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.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean; 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.Node;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.model.type.SlotLimit; import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.RoomRepository;

View File

@ -23,8 +23,11 @@ ext {
} }
dependencies { dependencies {
implementation project(":api")
implementation 'com.vaadin:vaadin-spring-boot-starter' implementation 'com.vaadin:vaadin-spring-boot-starter'
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test' 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.html.H2;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; 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.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import lombok.Getter; import lombok.Getter;
import ru.dragonestia.picker.cp.model.Room; import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.cp.model.User; import ru.dragonestia.picker.api.model.User;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class AddUsers extends Details { public class AddUsers extends Details {
@ -79,8 +76,7 @@ public class AddUsers extends Details {
try { try {
onCommit.accept(readAllUsers(), ignoreSlots.getValue()); onCommit.accept(readAllUsers(), ignoreSlots.getValue());
} catch (Error error) { } catch (Error error) {
Notification.show(error.getMessage(), 3000, Notification.Position.TOP_END) Notifications.error(error.getMessage());
.addThemeVariants(NotificationVariant.LUMO_ERROR);
} }
clear(); clear();

View File

@ -10,7 +10,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
public class NavPath extends HorizontalLayout{ public class NavPath extends HorizontalLayout{
public NavPath(Point root, Point... points) { private NavPath(Point root, Point... points) {
setWidth("100%"); setWidth("100%");
setAlignItems(Alignment.CENTER); setAlignItems(Alignment.CENTER);
getStyle().set("background-color", "#F3F3F3") getStyle().set("background-color", "#F3F3F3")
@ -58,5 +58,20 @@ public class NavPath extends HorizontalLayout{
return button; 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; 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.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog; 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.html.Paragraph;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; 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.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import lombok.Setter; import lombok.Setter;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.api.model.Node;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -51,14 +50,14 @@ public class NodeList extends VerticalLayout {
var temp = input.trim(); var temp = input.trim();
nodesGrid.setItems(cachedNodes.stream() nodesGrid.setItems(cachedNodes.stream()
.filter(node -> node.id().startsWith(temp)) .filter(node -> node.getId().startsWith(temp))
.toList()); .toList());
} }
private Grid<Node> createGrid() { private Grid<Node> createGrid() {
var grid = new Grid<>(Node.class, false); var grid = new Grid<>(Node.class, false);
grid.addColumn(Node::id).setHeader("Identifier"); grid.addColumn(Node::getId).setHeader("Identifier");
grid.addColumn(node -> node.mode().getName()).setHeader("Mode"); grid.addColumn(node -> node.getMode().getName()).setHeader("Mode");
grid.addComponentColumn(this::createManageButtons).setHeader("Manage"); grid.addComponentColumn(this::createManageButtons).setHeader("Manage");
return grid; return grid;
} }
@ -84,29 +83,28 @@ public class NodeList extends VerticalLayout {
} }
private void clickDetailsButton(Node node) { 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) { private void clickRemoveButton(Node node) {
var dialog = new Dialog("Confirm node deletion"); 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(); var inputField = new TextField();
inputField.setWidth("100%");
dialog.add(inputField); dialog.add(inputField);
{ // confirm { // confirm
var button = new Button("Confirm"); var button = new Button("Confirm");
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> { button.addClickListener(event -> {
if (!node.id().equals(inputField.getValue())) { if (!node.getId().equals(inputField.getValue())) {
Notification.show("Invalid input", 3000, Notification.Position.TOP_END) Notifications.error("Invalid input");
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
removeNode(node); removeNode(node);
Notification.show("Node '" + node.id() + "' was successfully removed!", 3000, Notification.Position.TOP_END) Notifications.success("Node <b>" + node.getId() + "</b> was successfully removed!");
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
dialog.close(); dialog.close();
}); });
@ -129,7 +127,7 @@ public class NodeList extends VerticalLayout {
private void removeNode(Node node) { private void removeNode(Node node) {
if (removeMethod != null) { 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.details.Details;
import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Span; 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.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.radiobutton.RadioButtonGroup; import com.vaadin.flow.component.radiobutton.RadioButtonGroup;
import com.vaadin.flow.component.radiobutton.RadioGroupVariant; 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.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.ComponentRenderer;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.cp.model.type.PickingMode; import ru.dragonestia.picker.api.model.type.PickingMode;
import java.util.function.Function; import java.util.function.Function;
@ -91,8 +89,7 @@ public class RegisterNode extends Details {
error = "Invalid node id format"; error = "Invalid node id format";
} }
Notification.show(error, 3000, Notification.Position.TOP_END) Notifications.error(error);
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
@ -100,13 +97,11 @@ public class RegisterNode extends Details {
var response = onSubmit.apply(node); var response = onSubmit.apply(node);
clear(); clear();
if (response.error()) { if (response.error()) {
Notification.show(response.reason(), 3000, Notification.Position.TOP_END) Notifications.error(response.reason());
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
Notification.show("Node was successfully registered", 3000, Notification.Position.TOP_END) Notifications.success("Node was successfully registered");
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} }
public record Response(boolean error, @Nullable String reason) {} 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.checkbox.Checkbox;
import com.vaadin.flow.component.details.Details; import com.vaadin.flow.component.details.Details;
import com.vaadin.flow.component.html.H2; 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.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.Autocomplete; import com.vaadin.flow.component.textfield.Autocomplete;
import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import ru.dragonestia.picker.cp.model.Room; import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.cp.model.type.SlotLimit;
import java.util.function.Function; import java.util.function.Function;
@ -45,7 +42,7 @@ public class RegisterRoom extends Details {
private TextField createNodeIdentifierField() { private TextField createNodeIdentifierField() {
var field = new TextField("Node identifier"); var field = new TextField("Node identifier");
field.setMinWidth(20, Unit.REM); field.setMinWidth(20, Unit.REM);
field.setValue(node.id()); field.setValue(node.getId());
field.setReadOnly(true); field.setReadOnly(true);
return field; return field;
} }
@ -106,23 +103,20 @@ public class RegisterRoom extends Details {
error = "Invalid room id format"; error = "Invalid room id format";
} }
Notification.show(error, 3000, Notification.Position.TOP_END) Notifications.error(error);
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return; 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()); room.setLocked(lockedField.getValue());
var response = onSubmit.apply(room); var response = onSubmit.apply(room);
clear(); clear();
if (response.error()) { if (response.error()) {
Notification.show(response.reason(), 3000, Notification.Position.TOP_END) Notifications.error(response.reason());
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
Notification.show("Room was successfully registered", 3000, Notification.Position.TOP_END) Notifications.success("Room was successfully registered");
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} }
public record Response(boolean error, @Nullable String reason) {} public record Response(boolean error, @Nullable String reason) {}

View File

@ -1,5 +1,6 @@
package ru.dragonestia.picker.cp.component; 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.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog; 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.html.Span;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; 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.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import lombok.Setter; 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.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -24,12 +23,12 @@ import java.util.function.Consumer;
public class RoomList extends VerticalLayout { public class RoomList extends VerticalLayout {
private final String nodeIdentifier; private final String nodeIdentifier;
private final Grid<RoomDTO> roomsGrid; private final Grid<Room.Short> roomsGrid;
private final TextField searchField; private final TextField searchField;
private List<RoomDTO> cachedRooms; private List<Room.Short> cachedRooms;
@Setter private Consumer<RoomDTO> removeMethod; @Setter private Consumer<Room.Short> removeMethod;
public RoomList(String nodeIdentifier, List<RoomDTO> buckets) { public RoomList(String nodeIdentifier, List<Room.Short> buckets) {
this.nodeIdentifier = nodeIdentifier; this.nodeIdentifier = nodeIdentifier;
cachedRooms = buckets; cachedRooms = buckets;
@ -57,9 +56,9 @@ public class RoomList extends VerticalLayout {
.toList()); .toList());
} }
private Grid<RoomDTO> createGrid() { private Grid<Room.Short> createGrid() {
var grid = new Grid<>(RoomDTO.class, false); var grid = new Grid<>(Room.Short.class, false);
grid.addColumn(RoomDTO::id).setHeader("Identifier"); grid.addColumn(Room.Short::id).setHeader("Identifier");
grid.addComponentColumn(room -> { grid.addComponentColumn(room -> {
var result = new Span(); var result = new Span();
if (room.slots() == -1) { if (room.slots() == -1) {
@ -84,7 +83,7 @@ public class RoomList extends VerticalLayout {
return grid; return grid;
} }
private HorizontalLayout createManageButtons(RoomDTO room) { private HorizontalLayout createManageButtons(Room.Short room) {
var layout = new HorizontalLayout(); var layout = new HorizontalLayout();
{ {
@ -104,18 +103,19 @@ public class RoomList extends VerticalLayout {
return layout; return layout;
} }
private void clickDetailsButton(RoomDTO bucket) { private void clickDetailsButton(Room.Short bucket) {
getUI().ifPresent(ui -> { getUI().ifPresent(ui -> {
ui.navigate("/nodes/" + nodeIdentifier + ui.navigate("/nodes/" + nodeIdentifier +
"/rooms/" + bucket.id()); "/rooms/" + bucket.id());
}); });
} }
private void clickRemoveButton(RoomDTO bucket) { private void clickRemoveButton(Room.Short bucket) {
var dialog = new Dialog("Confirm bucket deletion"); 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(); var inputField = new TextField();
inputField.setWidth("100%");
dialog.add(inputField); dialog.add(inputField);
{ // confirm { // confirm
@ -123,14 +123,12 @@ public class RoomList extends VerticalLayout {
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
button.addClickListener(event -> { button.addClickListener(event -> {
if (!bucket.id().equals(inputField.getValue())) { if (!bucket.id().equals(inputField.getValue())) {
Notification.show("Invalid input", 3000, Notification.Position.TOP_END) Notifications.error("Invalid input");
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
removeBucket(bucket); removeBucket(bucket);
Notification.show("Bucket '" + bucket.id() + "' was successfully removed!", 3000, Notification.Position.TOP_END) Notifications.success("Bucket <b>" + bucket.id() + "</b> was successfully removed!");
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
dialog.close(); dialog.close();
}); });
@ -146,12 +144,12 @@ public class RoomList extends VerticalLayout {
dialog.open(); dialog.open();
} }
public void update(List<RoomDTO> buckets) { public void update(List<Room.Short> buckets) {
cachedRooms = buckets; cachedRooms = buckets;
applySearch(searchField.getValue()); applySearch(searchField.getValue());
} }
private void removeBucket(RoomDTO bucket) { private void removeBucket(Room.Short bucket) {
if (removeMethod != null) { if (removeMethod != null) {
removeMethod.accept(bucket); 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.grid.Grid;
import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import ru.dragonestia.picker.cp.model.Room; import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.cp.model.User; import ru.dragonestia.picker.api.model.User;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -28,7 +28,7 @@ public class UserList extends VerticalLayout {
private Grid<User> createUsersGrid() { private Grid<User> createUsersGrid() {
var grid = new Grid<User>(); 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 grid.addColumn(user -> 0).setTextAlign(ColumnTextAlign.CENTER).setHeader("Linked with rooms") // TODO
.setFooter(occupancy); .setFooter(occupancy);
grid.addComponentColumn(user -> new Span("buttons")).setHeader("Manage"); // TODO grid.addComponentColumn(user -> new Span("buttons")).setHeader("Manage"); // TODO
@ -39,6 +39,12 @@ public class UserList extends VerticalLayout {
cachedUsers = users; cachedUsers = users;
usersGrid.setItems(users); usersGrid.setItems(users);
totalUsers.setText("Total users: " + users.size()); 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 @Bean
Supplier<RestTemplate> restTemplate(@Autowired RestTemplateBuilder builder) { Supplier<RestTemplate> restTemplateSupplier(@Autowired RestTemplateBuilder builder) {
return builder::build; 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.H1;
import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; 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("/") @Route("/")
public class HomePage extends VerticalLayout { public class HomePage extends VerticalLayout implements BeforeEnterObserver {
public HomePage() { public HomePage() {
super(); super();
@ -14,4 +15,9 @@ public class HomePage extends VerticalLayout {
add(new H1("Hello world!")); add(new H1("Hello world!"));
add(new Paragraph("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.H2;
import com.vaadin.flow.component.html.Hr; import com.vaadin.flow.component.html.Hr;
import com.vaadin.flow.component.html.Paragraph; 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.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; 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.RoomList;
import ru.dragonestia.picker.cp.component.NavPath; import ru.dragonestia.picker.cp.component.NavPath;
import ru.dragonestia.picker.cp.component.RegisterRoom; import ru.dragonestia.picker.cp.component.RegisterRoom;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.cp.util.RouteParamsExtractor;
import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import ru.dragonestia.picker.cp.repository.RoomRepository;
import ru.dragonestia.picker.cp.repository.NodeRepository;
import java.util.List; import java.util.List;
@Getter @Getter
@RequiredArgsConstructor
@PageTitle("Rooms") @PageTitle("Rooms")
@Route("/nodes/:nodeId") @Route("/nodes/:nodeId")
public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserver { public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserver {
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
private final RoomRepository roomRepository; private final RoomRepository roomRepository;
private final RouteParamsExtractor paramsExtractor;
private Node node; private Node node;
private RegisterRoom registerRoom; private RegisterRoom registerRoom;
private RoomList roomList; private RoomList roomList;
public NodeDetailsPage(@Autowired NodeRepository nodeRepository,
@Autowired RoomRepository roomRepository) {
this.nodeRepository = nodeRepository;
this.roomRepository = roomRepository;
}
@Override @Override
public void beforeEnter(BeforeEnterEvent event) { public void beforeEnter(BeforeEnterEvent event) {
var nodeIdOpt = event.getRouteParameters().get("nodeId"); node = paramsExtractor.extractNodeId(event);
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();
initComponents(node, roomRepository.all(node)); 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); printNodeDetails(node);
add(new Hr()); add(new Hr());
add(registerRoom = new RegisterRoom(node, (bucket) -> { add(registerRoom = new RegisterRoom(node, (room) -> {
try { try {
roomRepository.register(bucket); roomRepository.register(room);
return new RegisterRoom.Response(false, null); return new RegisterRoom.Response(false, null);
} catch (Error error) { } catch (Error error) {
return new RegisterRoom.Response(true, error.getMessage()); return new RegisterRoom.Response(true, error.getMessage());
@ -78,9 +60,9 @@ public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserv
} }
})); }));
add(new Hr()); add(new Hr());
add(roomList = new RoomList(node.id(), rooms)); add(roomList = new RoomList(node.getId(), rooms));
roomList.setRemoveMethod(bucket -> { roomList.setRemoveMethod(room -> {
roomRepository.remove(node, bucket); roomRepository.remove(node, room);
roomList.update(roomRepository.all(node)); roomList.update(roomRepository.all(node));
}); });
} }
@ -89,8 +71,8 @@ public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserv
add(new H2("Node details")); add(new H2("Node details"));
var layout = new VerticalLayout(); var layout = new VerticalLayout();
layout.add(new Html("<span>Identifier: <b>" + node.id() + "</b></span>")); layout.add(new Html("<span>Identifier: <b>" + node.getId() + "</b></span>"));
layout.add(new Html("<span>Mode: <b>" + node.mode().getName() + "</b></span>")); layout.add(new Html("<span>Mode: <b>" + node.getMode().getName() + "</b></span>"));
add(layout); add(layout);
} }

View File

@ -7,10 +7,11 @@ import com.vaadin.flow.router.Route;
import lombok.Getter; import lombok.Getter;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired; 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.NavPath;
import ru.dragonestia.picker.cp.component.NodeList; import ru.dragonestia.picker.cp.component.NodeList;
import ru.dragonestia.picker.cp.component.RegisterNode; import ru.dragonestia.picker.cp.component.RegisterNode;
import ru.dragonestia.picker.cp.repository.NodeRepository;
@Log4j2 @Log4j2
@Getter @Getter
@ -26,7 +27,7 @@ public class NodesPage extends VerticalLayout {
super(); super();
this.nodeRepository = nodeRepository; this.nodeRepository = nodeRepository;
add(new NavPath(new NavPath.Point("Nodes", "/nodes"))); add(NavPath.rootNodes());
add(registerNode = createRegisterNodeElement()); add(registerNode = createRegisterNodeElement());
add(new Hr()); add(new Hr());
add(nodeList = createNodeListElement()); add(nodeList = createNodeListElement());
@ -41,10 +42,7 @@ public class NodesPage extends VerticalLayout {
try { try {
nodeRepository.register(node); nodeRepository.register(node);
return new RegisterNode.Response(false, ""); return new RegisterNode.Response(false, "");
} catch (Error ex) { } catch (ApiException ex) {
return new RegisterNode.Response(true, ex.getMessage());
} catch (RuntimeException ex) {
log.throwing(ex);
return new RegisterNode.Response(true, ex.getMessage()); return new RegisterNode.Response(true, ex.getMessage());
} finally { } finally {
nodeList.update(nodeRepository.all()); 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.html.Paragraph;
import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon; 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.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; 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.AddUsers;
import ru.dragonestia.picker.cp.component.NavPath; 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.component.UserList;
import ru.dragonestia.picker.cp.model.Room; import ru.dragonestia.picker.cp.util.RouteParamsExtractor;
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 java.util.Collection; import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@RequiredArgsConstructor
@PageTitle("Room details") @PageTitle("Room details")
@Route("/nodes/:nodeId/rooms/:roomId") @Route("/nodes/:nodeId/rooms/:roomId")
public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserver { public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserver {
@ -37,6 +39,8 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
private final RoomRepository roomRepository; private final RoomRepository roomRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final RouteParamsExtractor paramsExtractor;
private Node node; private Node node;
private Room room; private Room room;
private AddUsers addUsers; private AddUsers addUsers;
@ -44,57 +48,16 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
private Button lockRoomButton; private Button lockRoomButton;
private VerticalLayout roomInfo; private VerticalLayout roomInfo;
@Autowired
public RoomDetailsPage(NodeRepository nodeRepository, RoomRepository roomRepository, UserRepository userRepository) {
this.nodeRepository = nodeRepository;
this.roomRepository = roomRepository;
this.userRepository = userRepository;
}
@Override @Override
public void beforeEnter(BeforeEnterEvent event) { public void beforeEnter(BeforeEnterEvent event) {
var nodeIdOpt = event.getRouteParameters().get("nodeId"); node = paramsExtractor.extractNodeId(event);
if (nodeIdOpt.isEmpty()) { room = paramsExtractor.extractRoomId(event, node);
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();
init(); init();
} }
private void init() { private void init() {
add(NavPath.toRoom(node.getId(), room.getId()));
add(new H2("Room details")); add(new H2("Room details"));
printRoomDetails(); printRoomDetails();
add(new Hr()); add(new Hr());
@ -108,7 +71,7 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
roomInfo.removeAll(); roomInfo.removeAll();
roomInfo.add(new Html("<span>Node identifier: <b>" + room.getNodeId() + "</b></span>")); 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>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>")); 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() { private void changeBucketLockedState() {
var newValue = !room.isLocked(); var newValue = !room.isLocked();
try { roomRepository.lock(room, newValue);
roomRepository.lock(room, newValue);
} catch (Error error) {
Notification.show(error.getMessage(), 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
return;
}
room.setLocked(newValue); room.setLocked(newValue);
setLockRoomButtonState(); setLockRoomButtonState();
updateRoomInfo(); updateRoomInfo();
Notification.show("Success", 3000, Notification.Position.TOP_END) Notifications.success("Success");
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} }
private void appendUsers(Room room, Collection<User> users, boolean ignoreLimitation) { 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() var newUsers = users.stream()
.filter(user -> { .filter(user -> {
if (user.id().matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$")) { if (user.getId().matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$")) {
return true; return true;
} }
@ -173,15 +129,12 @@ public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserv
if (validationFail.get()) { if (validationFail.get()) {
if (newUsers.isEmpty()) { 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) Notifications.error("All users entered were added because they do not comply with the rule for writing the user identifier");
.addThemeVariants(NotificationVariant.LUMO_ERROR);
} else { } 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) Notifications.warn("Not all users entered were added because they do not comply with the rule for writing the user identifier");
.addThemeVariants(NotificationVariant.LUMO_WARNING);
} }
} else { } else {
Notification.show("Success", 3000, Notification.Position.TOP_END) Notifications.success("Success");
.addThemeVariants(NotificationVariant.LUMO_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 com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import ru.dragonestia.picker.cp.model.Node; import org.springframework.http.HttpMethod;
import ru.dragonestia.picker.cp.repository.NodeRepository; import ru.dragonestia.picker.api.exception.InvalidNodeIdentifierException;
import ru.dragonestia.picker.cp.repository.impl.response.NodeDetailsResponse; import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.cp.repository.impl.response.NodeListResponse; import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.cp.repository.impl.response.NodeRegisterResponse; 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.List;
import java.util.Optional; import java.util.Optional;
@ -21,41 +23,30 @@ public class NodeRepositoryImpl implements NodeRepository {
private final RestUtil rest; private final RestUtil rest;
@Override @Override
public void register(Node node) { public void register(Node node) throws InvalidNodeIdentifierException, NodeAlreadyExistException {
NodeRegisterResponse response; rest.query("nodes", HttpMethod.POST, params -> {
try { params.put("nodeId", node.getId());
response = rest.post(URI.create("nodes"), params.put("method", node.getMode().name());
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());
}
} }
@Override @Override
public List<Node> all() { public List<Node> all() {
return rest.get(URI.create("nodes"), NodeListResponse.class).nodes(); return rest.query("nodes", HttpMethod.GET, NodeListResponse.class, params -> {}).nodes();
} }
@Override @Override
public Optional<Node> find(String nodeId) { public Optional<Node> find(String nodeId) {
try { 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()); return Optional.of(response.node());
} catch (Exception ex) { } catch (NodeNotFoundException ex) {
return Optional.empty(); return Optional.empty();
} }
} }
@Override @Override
public void remove(String nodeId) { 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; package ru.dragonestia.picker.cp.repository.impl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate; 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.net.URI;
import java.util.Collection; import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -18,67 +18,41 @@ import java.util.function.Supplier;
public class RestUtil { public class RestUtil {
private final URI serverUrl; private final URI serverUrl;
private final Supplier<RestTemplate> restTemplate; private final Supplier<RestTemplate> restTemplateSupplier;
public <T> T get(URI uri, Class<T> responseType) { public void query(String uri, HttpMethod method) {
var template = restTemplate.get(); query(uri, method, ParamsConsumer.NONE);
return Objects.requireNonNull(template.getForObject(serverUrl.resolve(uri), responseType));
} }
public <T> ResponseEntity<T> getEntity(URI uri, Class<T> responseType) { public void query(String uri, HttpMethod method, ParamsConsumer paramsConsumer) {
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) {
var params = new HashMap<String, String>(); var params = new HashMap<String, String>();
paramsConsumer.accept(params); paramsConsumer.accept(params);
var template = restTemplate.get(); var template = restTemplateSupplier.get();
return Objects.requireNonNull(template.getForObject(buildPath(uri, params.keySet()), try {
responseType, template.exchange(buildPath(uri, params.keySet()), method, null, String.class, params);
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>(); var params = new HashMap<String, String>();
paramsConsumer.accept(params); paramsConsumer.accept(params);
var template = restTemplate.get(); var template = restTemplateSupplier.get();
return Objects.requireNonNull(template.postForObject(buildPath(uri, params.keySet()), try {
null, return template.exchange(buildPath(uri, params.keySet()), method, null, clazz, params).getBody();
responseType, } catch (HttpClientErrorException ex) {
params)); throw ExceptionFactory.of(Objects.requireNonNull(ex.getResponseBodyAs(ErrorResponse.class)));
}
} }
public <T> ResponseEntity<T> postEntity(URI uri, Class<T> responseType, Consumer<Map<String, String>> paramsConsumer) { private String buildPath(String uri, Collection<String> paramKeys) {
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) {
var path = new StringBuilder(serverUrl.resolve(uri) + "?"); var path = new StringBuilder(serverUrl.resolve(uri) + "?");
int left = paramKeys.size(); int left = paramKeys.size();
for (var key: paramKeys) { for (var key: paramKeys) {
@ -92,4 +66,9 @@ public class RestUtil {
} }
return path.toString(); 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 com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.http.HttpMethod;
import ru.dragonestia.picker.cp.model.Room; import ru.dragonestia.picker.api.exception.InvalidRoomIdentifierException;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.cp.model.dto.RoomDTO; import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.cp.repository.RoomRepository; import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.cp.repository.impl.response.RoomInfoResponse; import ru.dragonestia.picker.api.model.Node;
import ru.dragonestia.picker.cp.repository.impl.response.RoomListResponse; import ru.dragonestia.picker.api.model.Room;
import ru.dragonestia.picker.cp.repository.impl.response.RoomRegisterResponse; 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.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@Log4j2 @Log4j2
@ -25,76 +25,44 @@ public class RoomRepositoryImpl implements RoomRepository {
private final RestUtil rest; private final RestUtil rest;
@Override @Override
public List<RoomDTO> all(Node node) { public void register(Room room) throws NodeNotFoundException, InvalidRoomIdentifierException, RoomAlreadyExistException {
var entity = rest.getEntity(URI.create("/nodes/" + node.id() + "/rooms"), rest.query("/nodes/" + room.getNodeId() + "/rooms", HttpMethod.POST, params -> {
RoomListResponse.class); params.put("roomId", room.getId());
params.put("slots", Integer.toString(room.getSlots()));
if (entity.getStatusCode().value() == 404) { params.put("payload", room.getPayload());
throw new Error("Node with identifier '" + node.id() + "' does not exists'"); params.put("locked", Boolean.toString(room.isLocked()));
} });
if (!entity.hasBody()) {
throw new Error("Room list did not present");
}
return Objects.requireNonNull(entity.getBody()).rooms();
} }
@Override @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 { try {
var response = rest.post(URI.create("/nodes/" + room.getNodeId() + "/rooms"), var response = rest.query("/nodes/" + node.getId() + "/rooms/" + roomId, HttpMethod.GET, RoomInfoResponse.class, map -> {});
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 -> {});
return Optional.of(response.room()); return Optional.of(response.room());
} catch (Exception ex) { } catch (RoomNotFoundException ex) {
return Optional.empty(); return Optional.empty();
} }
} }
@Override @Override
public void lock(Room room, boolean value) { public void lock(Room room, boolean value) throws NodeNotFoundException, RoomNotFoundException {
try { rest.query("/nodes/%s/rooms/%s/lock".formatted(room.getNodeId(), room.getId()), HttpMethod.PUT, params -> {
rest.post(URI.create(room.createApiURI() + "/lock"), Boolean.class, params -> { params.put("newState", Boolean.toString(value));
params.put("newState", Boolean.toString(value)); });
});
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Error when changing room locked state");
}
} }
} }

View File

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

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