Renamed Bucket to Room

This commit is contained in:
Andrey Terentev 2023-12-27 22:06:09 +07:00
parent a8348dcd3d
commit 3de835cc6f
69 changed files with 999 additions and 998 deletions

View File

@ -59,8 +59,9 @@ For use api methods from main application you need add http headers with usernam
and password (`Room_Picker-Password`). and password (`Room_Picker-Password`).
Example: Example:
```http request ```http request
GET http://localhost:8080/nodes/hub/buckets/test-3/users GET http://localhost:8080/nodes/hub/rooms/test-3/users
RoomPicker-User: admin RoomPicker-User: admin
RoomPicker-Password: admin RoomPicker-Password: admin
``` ```

View File

@ -6,8 +6,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class LoadBalancerApplication { public class LoadBalancerApplication {
public record Test(String text) {}
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(LoadBalancerApplication.class, args); SpringApplication.run(LoadBalancerApplication.class, args);
} }

View File

@ -8,12 +8,12 @@ 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.interceptor.DebugInterceptor; import ru.dragonestia.picker.interceptor.DebugInterceptor;
import ru.dragonestia.picker.model.Bucket; 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.LoadBalancingMethod; 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.BucketRepository; import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
@ -27,7 +27,7 @@ import java.util.UUID;
public class TestConfig implements WebMvcConfigurer { public class TestConfig implements WebMvcConfigurer {
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
private final BucketRepository bucketRepository; private final RoomRepository roomRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final Random rand = new Random(0); private final Random rand = new Random(0);
@ -39,29 +39,29 @@ public class TestConfig implements WebMvcConfigurer {
@Bean @Bean
void createNodes() { void createNodes() {
createNodeWithContent(new Node("game-servers", LoadBalancingMethod.ROUND_ROBIN)); createNodeWithContent(new Node("game-servers", PickingMode.ROUND_ROBIN));
createNodeWithContent(new Node("game-lobbies", LoadBalancingMethod.LEAST_PICKED)); createNodeWithContent(new Node("game-lobbies", PickingMode.LEAST_PICKED));
createNodeWithContent(new Node("hub", LoadBalancingMethod.SEQUENTIAL_FILLING)); createNodeWithContent(new Node("hub", PickingMode.SEQUENTIAL_FILLING));
} }
private void createNodeWithContent(Node node) { private void createNodeWithContent(Node node) {
nodeRepository.createNode(node); nodeRepository.create(node);
for (int i = 1; i <= 5; i++) { for (int i = 1; i <= 5; i++) {
var slots = 5 * i; var slots = 5 * i;
var bucket = Bucket.create("test-" + i, node, SlotLimit.of(slots), "Some payload"); var room = Room.create("test-" + i, node, SlotLimit.of(slots), "Some payload");
bucketRepository.createBucket(bucket); roomRepository.create(room);
for (int j = 0, n = rand.nextInt(slots + 1); j < n; j++) { for (int j = 0, n = rand.nextInt(slots + 1); j < n; j++) {
var user = new User("test-user-" + rand.nextInt(20)); var user = new User("test-user-" + rand.nextInt(20));
userRepository.linkWithBucket(bucket, List.of(user), false); userRepository.linkWithRoom(room, List.of(user), false);
} }
} }
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
var bucket = Bucket.create(randomUUID().toString(), node, SlotLimit.unlimited(), "Some payload"); var room = Room.create(randomUUID().toString(), node, SlotLimit.unlimited(), "Some payload");
bucket.setLocked((i & 1) == 0); room.setLocked((i & 1) == 0);
bucketRepository.createBucket(bucket); roomRepository.create(room);
} }
} }

View File

@ -1,117 +0,0 @@
package ru.dragonestia.picker.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.BucketInfoResponse;
import ru.dragonestia.picker.controller.response.BucketListResponse;
import ru.dragonestia.picker.controller.response.BucketRegisterResponse;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.service.BucketService;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.Objects;
@Log4j2
@RestController
@RequestMapping("/nodes/{nodeIdentifier}/buckets")
@RequiredArgsConstructor
public class BucketController {
private final NodeService nodeService;
private final BucketService bucketService;
@GetMapping
ResponseEntity<BucketListResponse> allBuckets(@PathVariable(name = "nodeIdentifier") String nodeId) {
var nodeOpt = nodeService.findNode(nodeId);
return nodeOpt.map(node -> ResponseEntity.ok(new BucketListResponse(nodeId,
bucketService.allBuckets(node).stream()
.map(bucket -> new BucketListResponse.BucketDTO(bucket.getIdentifier(), bucket.getSlots().getSlots(), bucket.isLocked()))
.toList()
))).orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
ResponseEntity<BucketRegisterResponse> registerBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@RequestParam(name = "identifier") String bucketId,
@RequestParam(name = "slots") int slots,
@RequestParam(name = "payload") String payload,
@RequestParam(name = "locked", defaultValue = "false") boolean locked) {
var nodeOpt = nodeService.findNode(nodeId);
if (nodeOpt.isEmpty()) {
return ResponseEntity.status(404)
.body(new BucketRegisterResponse(false, "Node does not exist"));
}
var bucket = Bucket.create(bucketId, nodeOpt.get(), SlotLimit.of(slots), payload);
bucket.setLocked(locked);
try {
bucketService.createBucket(bucket);
return ResponseEntity.ok(new BucketRegisterResponse(true, ""));
} catch (Error error) {
return ResponseEntity.status(400).body(new BucketRegisterResponse(false, error.getMessage()));
} catch (Exception ex) {
return ResponseEntity.status(500).body(new BucketRegisterResponse(false, ex.getMessage()));
}
}
@DeleteMapping("/{identifier}")
ResponseEntity<?> removeBucket(@PathVariable("nodeIdentifier") String nodeId,
@PathVariable("identifier") String bucketId) {
if (!NamingValidator.validateNodeIdentifier(nodeId) || !NamingValidator.validateBucketIdentifier(bucketId)) {
return ResponseEntity.ok().build();
}
var nodeOpt = nodeService.findNode(nodeId);
nodeOpt.flatMap(node -> bucketService.findBucket(node, bucketId))
.ifPresent(bucketService::removeBucket);
return ResponseEntity.ok().build();
}
@GetMapping("/{identifier}")
ResponseEntity<BucketInfoResponse> info(@PathVariable("nodeIdentifier") String nodeId,
@PathVariable("identifier") String bucketId) {
if (!NamingValidator.validateNodeIdentifier(nodeId) || !NamingValidator.validateBucketIdentifier(bucketId)) {
return ResponseEntity.ok().build();
}
var nodeOpt = nodeService.findNode(nodeId);
if (nodeOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
var bucketOpt = bucketService.findBucket(Objects.requireNonNull(nodeOpt.get()), bucketId);
return bucketOpt.map(bucket -> ResponseEntity.ok(new BucketInfoResponse(bucket)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping("/{identifier}/lock")
ResponseEntity<Boolean> lockBucket(@PathVariable("nodeIdentifier") String nodeId,
@PathVariable("identifier") String bucketId,
@RequestParam(name = "state") boolean value) {
if (!NamingValidator.validateNodeIdentifier(nodeId) || !NamingValidator.validateBucketIdentifier(bucketId)) {
return ResponseEntity.notFound().build();
}
var nodeOpt = nodeService.findNode(nodeId);
if (nodeOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
var bucketOpt = bucketService.findBucket(Objects.requireNonNull(nodeOpt.get()), bucketId);
if (bucketOpt.isEmpty()) {
return ResponseEntity.notFound().build();
}
var bucket = bucketOpt.get();
bucket.setLocked(value);
return ResponseEntity.ok(true);
}
}

View File

@ -8,7 +8,7 @@ import ru.dragonestia.picker.controller.response.NodeDetailsResponse;
import ru.dragonestia.picker.controller.response.NodeListResponse; import ru.dragonestia.picker.controller.response.NodeListResponse;
import ru.dragonestia.picker.controller.response.NodeRegisterResponse; import ru.dragonestia.picker.controller.response.NodeRegisterResponse;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.type.LoadBalancingMethod; import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.service.NodeService; import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.util.NamingValidator; import ru.dragonestia.picker.util.NamingValidator;
@ -21,15 +21,15 @@ public class NodeController {
@GetMapping @GetMapping
NodeListResponse allNodes() { NodeListResponse allNodes() {
return new NodeListResponse(nodeService.allNodes()); return new NodeListResponse(nodeService.all());
} }
@PostMapping @PostMapping
NodeRegisterResponse registerNode(@RequestParam(name = "identifier") String identifier, NodeRegisterResponse registerNode(@RequestParam(name = "nodeId") String nodeId,
@RequestParam(name = "method") LoadBalancingMethod method) { @RequestParam(name = "method") PickingMode method) {
try { try {
nodeService.createNode(new Node(identifier, method)); nodeService.create(new Node(nodeId, method));
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
return new NodeRegisterResponse(false, ex.getMessage()); return new NodeRegisterResponse(false, ex.getMessage());
} catch (Error error) { } catch (Error error) {
@ -39,25 +39,25 @@ public class NodeController {
return new NodeRegisterResponse(true, ""); return new NodeRegisterResponse(true, "");
} }
@GetMapping("/{identifier}") @GetMapping("/{nodeId}")
ResponseEntity<NodeDetailsResponse> nodeDetails(@PathVariable("identifier") String identifier) { ResponseEntity<NodeDetailsResponse> nodeDetails(@PathVariable("nodeId") String nodeId) {
if (!NamingValidator.validateNodeIdentifier(identifier)) { if (!NamingValidator.validateNodeId(nodeId)) {
return new ResponseEntity<>(HttpStatusCode.valueOf(404)); return new ResponseEntity<>(HttpStatusCode.valueOf(404));
} }
var nodeOpt = nodeService.findNode(identifier); var nodeOpt = nodeService.find(nodeId);
return nodeOpt.map(node -> ResponseEntity.ok(new NodeDetailsResponse(node))) return nodeOpt.map(node -> ResponseEntity.ok(new NodeDetailsResponse(node)))
.orElseGet(() -> new ResponseEntity<>(HttpStatusCode.valueOf(404))); .orElseGet(() -> new ResponseEntity<>(HttpStatusCode.valueOf(404)));
} }
@DeleteMapping("/{identifier}") @DeleteMapping("/{nodeId}")
ResponseEntity<?> removeNode(@PathVariable("identifier") String identifier) { ResponseEntity<?> removeNode(@PathVariable("nodeId") String nodeId) {
if (!NamingValidator.validateNodeIdentifier(identifier)) { if (!NamingValidator.validateNodeId(nodeId)) {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
var nodeOpt = nodeService.findNode(identifier); var nodeOpt = nodeService.find(nodeId);
nodeOpt.ifPresent(nodeService::removeNode); nodeOpt.ifPresent(nodeService::remove);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }

View File

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

View File

@ -1,111 +0,0 @@
package ru.dragonestia.picker.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.BucketUserListResponse;
import ru.dragonestia.picker.controller.response.LinkUsersWithBucketResponse;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.service.BucketService;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.service.UserService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.LinkedList;
import java.util.Objects;
@RequiredArgsConstructor
@RestController
@RequestMapping("/nodes/{nodeIdentifier}/buckets/{bucketIdentifier}/users")
public class UserBucketController {
private final NodeService nodeService;
private final BucketService bucketService;
private final UserService userService;
@GetMapping
ResponseEntity<BucketUserListResponse> usersInsideBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@PathVariable(name = "bucketIdentifier") String bucketId) {
Bucket bucket;
try {
var temp = getNodeAndBucket(nodeId, bucketId);
bucket = temp.bucket();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
var users = userService.getBucketUsers(bucket);
return ResponseEntity.ok(new BucketUserListResponse(bucket.getSlots().getSlots(), users.size(), users));
}
@PostMapping
ResponseEntity<LinkUsersWithBucketResponse> linkUserWithBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@PathVariable(name = "bucketIdentifier") String bucketId,
@RequestParam(name = "users") String userIds,
@RequestParam(name = "force") boolean force) {
Bucket bucket;
try {
var temp = getNodeAndBucket(nodeId, bucketId);
bucket = temp.bucket();
} catch (Error error) {
return ResponseEntity.status(404).body(new LinkUsersWithBucketResponse(false, error.getMessage()));
}
var list = new LinkedList<User>();
for (var username: userIds.split(",")) {
if (!NamingValidator.validateUserIdentifier(username)) continue;
list.add(new User(username));
}
try {
userService.linkUsersWithBucket(bucket, list, force);
} catch (Error error) {
return ResponseEntity.status(400).body(new LinkUsersWithBucketResponse(false, error.getMessage()));
}
return ResponseEntity.ok(new LinkUsersWithBucketResponse(true, "Success"));
}
@DeleteMapping
ResponseEntity<?> unlinkUsersForBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@PathVariable(name = "bucketIdentifier") String bucketId,
@RequestParam(name = "users") String userIdentifiers) {
Node node;
Bucket bucket;
try {
var temp = getNodeAndBucket(nodeId, bucketId);
node = temp.node();
bucket = temp.bucket();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
return null;
}
private record NodeAndBucket(Node node, Bucket bucket) {}
private NodeAndBucket getNodeAndBucket(String nodeId, String bucketId) {
if (!NamingValidator.validateNodeIdentifier(nodeId) || !NamingValidator.validateBucketIdentifier(bucketId)) {
throw new Error();
}
var nodeOpt = nodeService.findNode(nodeId);
if (nodeOpt.isEmpty()) {
throw new Error();
}
var bucketOpt = bucketService.findBucket(Objects.requireNonNull(nodeOpt.get()), bucketId);
if (bucketOpt.isEmpty()) {
throw new Error();
}
return new NodeAndBucket(nodeOpt.get(), bucketOpt.get());
}
}

View File

@ -0,0 +1,111 @@
package ru.dragonestia.picker.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.controller.response.RoomUserListResponse;
import ru.dragonestia.picker.controller.response.LinkUsersWithRoomResponse;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.service.UserService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.LinkedList;
import java.util.Objects;
@RequiredArgsConstructor
@RestController
@RequestMapping("/nodes/{nodeId}/rooms/{roomId}/users")
public class UserRoomController {
private final NodeService nodeService;
private final RoomService roomService;
private final UserService userService;
@GetMapping
ResponseEntity<RoomUserListResponse> usersInsideRoom(@PathVariable(name = "nodeId") String nodeId,
@PathVariable(name = "roomId") String bucketId) {
Room room;
try {
var temp = getNodeAndRoom(nodeId, bucketId);
room = temp.room();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
var users = userService.getRoomUsers(room);
return ResponseEntity.ok(new RoomUserListResponse(room.getSlots().getSlots(), users.size(), users));
}
@PostMapping
ResponseEntity<LinkUsersWithRoomResponse> linkUserWithRoom(@PathVariable(name = "nodeId") String nodeId,
@PathVariable(name = "roomId") String roomId,
@RequestParam(name = "userIds") String userIds,
@RequestParam(name = "force") boolean force) {
Room room;
try {
var temp = getNodeAndRoom(nodeId, roomId);
room = temp.room();
} catch (Error error) {
return ResponseEntity.status(404).body(new LinkUsersWithRoomResponse(false, error.getMessage()));
}
var list = new LinkedList<User>();
for (var username: userIds.split(",")) {
if (!NamingValidator.validateUserId(username)) continue;
list.add(new User(username));
}
try {
userService.linkUsersWithRoom(room, list, force);
} catch (Error error) {
return ResponseEntity.status(400).body(new LinkUsersWithRoomResponse(false, error.getMessage()));
}
return ResponseEntity.ok(new LinkUsersWithRoomResponse(true, "Success"));
}
@DeleteMapping
ResponseEntity<?> unlinkUsersForBucket(@PathVariable(name = "nodeId") String nodeId,
@PathVariable(name = "roomId") String roomId,
@RequestParam(name = "userIds") String userIds) {
Node node;
Room room;
try {
var temp = getNodeAndRoom(nodeId, roomId);
node = temp.node();
room = temp.room();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
return null;
}
private record NodeAndRoom(Node node, Room room) {}
private NodeAndRoom getNodeAndRoom(String nodeId, String roomId) {
if (!NamingValidator.validateNodeId(nodeId) || !NamingValidator.validateRoomId(roomId)) {
throw new Error();
}
var nodeOpt = nodeService.find(nodeId);
if (nodeOpt.isEmpty()) {
throw new Error();
}
var roomOpt = roomService.find(Objects.requireNonNull(nodeOpt.get()), roomId);
if (roomOpt.isEmpty()) {
throw new Error();
}
return new NodeAndRoom(nodeOpt.get(), roomOpt.get());
}
}

View File

@ -1,5 +0,0 @@
package ru.dragonestia.picker.controller.response;
import ru.dragonestia.picker.model.Bucket;
public record BucketInfoResponse(Bucket bucket) {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
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

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

View File

@ -4,4 +4,4 @@ import ru.dragonestia.picker.model.User;
import java.util.List; import java.util.List;
public record BucketUserListResponse(int slots, int usedSlots, List<User> users) {} public record RoomUserListResponse(int slots, int usedSlots, List<User> users) {}

View File

@ -1,13 +1,13 @@
package ru.dragonestia.picker.model; package ru.dragonestia.picker.model;
import lombok.NonNull; import lombok.NonNull;
import ru.dragonestia.picker.model.type.LoadBalancingMethod; import ru.dragonestia.picker.model.type.PickingMode;
public record Node(@NonNull String identifier, @NonNull LoadBalancingMethod method) { public record Node(@NonNull String id, @NonNull PickingMode mode) {
@Override @Override
public int hashCode() { public int hashCode() {
return identifier.hashCode(); return id.hashCode();
} }
@Override @Override
@ -15,7 +15,7 @@ public record Node(@NonNull String identifier, @NonNull LoadBalancingMethod meth
if (object == this) return true; if (object == this) return true;
if (object == null) return false; if (object == null) return false;
if (object instanceof Node other) { if (object instanceof Node other) {
return identifier.equals(other.identifier); return id.equals(other.id);
} }
return false; return false;
} }

View File

@ -7,16 +7,16 @@ import ru.dragonestia.picker.model.type.SlotLimit;
@Getter @Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class Bucket { public class Room {
private final String identifier; private final String id;
private final String nodeIdentifier; private final String nodeId;
private final SlotLimit slots; private final SlotLimit slots;
private final String payload; private final String payload;
private boolean locked = false; private boolean locked = false;
public static Bucket create(String identifier, Node node, SlotLimit limit, String payload) { public static Room create(String roomId, Node node, SlotLimit limit, String payload) {
return new Bucket(identifier, node.identifier(), limit, payload); return new Room(roomId, node.id(), limit, payload);
} }
public void setLocked(boolean value) { public void setLocked(boolean value) {
@ -31,15 +31,15 @@ public class Bucket {
@Override @Override
public int hashCode() { public int hashCode() {
return identifier.hashCode(); return id.hashCode();
} }
@Override @Override
public boolean equals(Object object) { public boolean equals(Object object) {
if (object == this) return true; if (object == this) return true;
if (object == null) return false; if (object == null) return false;
if (object instanceof Bucket other) { if (object instanceof Room other) {
return identifier.equals(other.identifier); return id.equals(other.id);
} }
return false; return false;
} }

View File

@ -2,11 +2,11 @@ package ru.dragonestia.picker.model;
import lombok.NonNull; import lombok.NonNull;
public record User(@NonNull String identifier) { public record User(@NonNull String id) {
@Override @Override
public int hashCode() { public int hashCode() {
return identifier.hashCode(); return id.hashCode();
} }
@Override @Override
@ -14,7 +14,7 @@ public record User(@NonNull String identifier) {
if (object == this) return true; if (object == this) return true;
if (object == null) return false; if (object == null) return false;
if (object instanceof User other) { if (object instanceof User other) {
return identifier.equals(other.identifier); return id.equals(other.id);
} }
return false; return false;
} }

View File

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

View File

@ -1,32 +0,0 @@
package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public interface BucketRepository {
void createBucket(Bucket bucket);
void removeBucket(Bucket bucket);
Optional<Bucket> findBucket(Node node, String identifier);
List<Bucket> all(Node node);
default int countAvailableBuckets(Node node) {
return countAvailableBuckets(node, 0);
}
int countAvailableBuckets(Node node, int requiredSlots);
Optional<Bucket> pickFreeBucket(Node node, Collection<User> users);
void onCreateNode(Node node);
void onRemoveNode(Node node);
}

View File

@ -7,11 +7,11 @@ import java.util.Optional;
public interface NodeRepository { public interface NodeRepository {
void createNode(Node node); void create(Node node);
void deleteNode(Node node); void delete(Node node);
Optional<Node> findNode(String nodeId); Optional<Node> find(String nodeId);
List<Node> all(); List<Node> all();
} }

View File

@ -0,0 +1,32 @@
package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public interface RoomRepository {
void create(Room room);
void remove(Room room);
Optional<Room> find(Node node, String identifier);
List<Room> all(Node node);
default int countAvailable(Node node) {
return countAvailable(node, 0);
}
int countAvailable(Node node, int requiredSlots);
Optional<Room> pickFree(Node node, Collection<User> users);
void onCreateNode(Node node);
void onRemoveNode(Node node);
}

View File

@ -1,6 +1,6 @@
package ru.dragonestia.picker.repository; package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.model.Bucket; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import java.util.Collection; import java.util.Collection;
@ -9,13 +9,13 @@ import java.util.Map;
public interface UserRepository { public interface UserRepository {
Map<User, Boolean> linkWithBucket(Bucket bucket, Collection<User> users, boolean force); Map<User, Boolean> linkWithRoom(Room room, Collection<User> users, boolean force);
int unlinkWithBucket(Bucket bucket, Collection<User> users); int unlinkWithRoom(Room room, Collection<User> users);
List<Bucket> findAllLinkedUserBuckets(User user); List<Room> findAllLinkedUserRooms(User user);
void onRemoveBucket(Bucket bucket); void onRemoveRoom(Room room);
List<User> usersOf(Bucket bucket); List<User> usersOf(Room room);
} }

View File

@ -1,131 +0,0 @@
package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.BucketRepository;
import ru.dragonestia.picker.repository.UserRepository;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Repository
@RequiredArgsConstructor
public class BucketRepositoryImpl implements BucketRepository {
private final UserRepository userRepository;
private final Map<Node, Buckets> node2bucketsMap = new ConcurrentHashMap<>();
@Override
public void createBucket(Bucket bucket) {
var nodeId = bucket.getNodeIdentifier();
synchronized (node2bucketsMap) {
var node = node2bucketsMap.keySet().stream()
.filter(n -> bucket.getNodeIdentifier().equals(n.identifier()))
.findFirst();
if (node.isEmpty()) {
throw new IllegalArgumentException("Node '" + nodeId + "' does not exist");
}
var buckets = node2bucketsMap.get(node.get());
if (buckets.containsKey(bucket.getIdentifier())) {
throw new IllegalArgumentException("Bucket already exists");
}
buckets.put(bucket.getIdentifier(), new BucketContainer(bucket, new AtomicInteger(0)));
}
}
@Override
public void removeBucket(Bucket bucket) {
var nodeId = bucket.getNodeIdentifier();
var node = node2bucketsMap.keySet().stream()
.filter(n -> bucket.getNodeIdentifier().equals(n.identifier()))
.findFirst();
synchronized (node2bucketsMap) {
if (node.isEmpty()) {
throw new IllegalArgumentException("Node '" + nodeId + "' does not exist");
}
node2bucketsMap.get(node.get()).remove(bucket.getIdentifier());
}
userRepository.onRemoveBucket(bucket);
}
@Override
public Optional<Bucket> findBucket(Node node, String identifier) {
synchronized (node2bucketsMap) {
if (!node2bucketsMap.containsKey(node)) {
throw new IllegalArgumentException("Node '" + node.identifier() + "' does not exist");
}
var result = node2bucketsMap.get(node).getOrDefault(identifier, null);
return result == null? Optional.empty() : Optional.of(result.bucket());
}
}
@Override
public List<Bucket> all(Node node) {
synchronized (node2bucketsMap) {
return node2bucketsMap.get(node).values().stream().map(BucketContainer::bucket).toList();
}
}
@Override
public int countAvailableBuckets(Node node, int requiredSlots) {
return (int) node2bucketsMap.get(node).values().stream()
.filter(bucket -> bucket.isAvailable(requiredSlots))
.count();
}
@Override
public Optional<Bucket> pickFreeBucket(Node node, Collection<User> users) {
synchronized (node2bucketsMap) {
if (!node2bucketsMap.containsKey(node)) {
throw new IllegalArgumentException("Node '" + node.identifier() + "' does not exist");
}
var requiredSlots = users.size();
var container = node2bucketsMap.get(node).values().stream() // TODO: pick bucket with used node balancing method
.filter(b -> b.isAvailable(requiredSlots))
.findFirst();
if (container.isPresent()) {
var cont = container.get();
var addedUsers = userRepository.linkWithBucket(cont.bucket(), users, false);
cont.used().getAndAdd((int) addedUsers.values().stream().filter(Boolean.TRUE::equals).count());
}
return container.map(BucketContainer::bucket);
}
}
@Override
public void onCreateNode(Node node) {
synchronized (node2bucketsMap) {
node2bucketsMap.put(node, new Buckets());
}
}
@Override
public void onRemoveNode(Node node) {
synchronized (node2bucketsMap) {
node2bucketsMap.remove(node);
}
}
private record BucketContainer(Bucket bucket, AtomicInteger used) {
public boolean isAvailable(int requiredSlots) {
return bucket.isAvailable(used.get(), requiredSlots);
}
}
private static class Buckets extends LinkedHashMap<String, BucketContainer> {}
}

View File

@ -3,7 +3,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.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.BucketRepository; import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.NodeRepository;
import java.util.List; import java.util.List;
@ -15,33 +15,33 @@ import java.util.concurrent.ConcurrentHashMap;
@RequiredArgsConstructor @RequiredArgsConstructor
public class NodeRepositoryImpl implements NodeRepository { public class NodeRepositoryImpl implements NodeRepository {
private final BucketRepository bucketRepository; private final RoomRepository roomRepository;
private final Map<String, Node> nodeMap = new ConcurrentHashMap<>(); private final Map<String, Node> nodeMap = new ConcurrentHashMap<>();
@Override @Override
public void createNode(Node node) { public void create(Node node) {
synchronized (nodeMap) { synchronized (nodeMap) {
if (nodeMap.containsKey(node.identifier())) { if (nodeMap.containsKey(node.id())) {
throw new IllegalArgumentException("Node with id '" + node.identifier() + "' already exists"); throw new IllegalArgumentException("Node with id '" + node.id() + "' already exists");
} }
nodeMap.put(node.identifier(), node); nodeMap.put(node.id(), node);
} }
bucketRepository.onCreateNode(node); roomRepository.onCreateNode(node);
} }
@Override @Override
public void deleteNode(Node node) { public void delete(Node node) {
synchronized (nodeMap) { synchronized (nodeMap) {
nodeMap.remove(node.identifier()); nodeMap.remove(node.id());
} }
bucketRepository.onRemoveNode(node); roomRepository.onRemoveNode(node);
} }
@Override @Override
public Optional<Node> findNode(String nodeId) { public Optional<Node> find(String nodeId) {
synchronized (nodeMap) { synchronized (nodeMap) {
return nodeMap.containsKey(nodeId)? Optional.of(nodeMap.get(nodeId)) : Optional.empty(); return nodeMap.containsKey(nodeId)? Optional.of(nodeMap.get(nodeId)) : Optional.empty();
} }

View File

@ -0,0 +1,131 @@
package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.UserRepository;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Repository
@RequiredArgsConstructor
public class RoomRepositoryImpl implements RoomRepository {
private final UserRepository userRepository;
private final Map<Node, Rooms> node2roomsMap = new ConcurrentHashMap<>();
@Override
public void create(Room room) {
var nodeId = room.getNodeId();
synchronized (node2roomsMap) {
var node = node2roomsMap.keySet().stream()
.filter(n -> room.getNodeId().equals(n.id()))
.findFirst();
if (node.isEmpty()) {
throw new IllegalArgumentException("Node '" + nodeId + "' does not exist");
}
var rooms = node2roomsMap.get(node.get());
if (rooms.containsKey(room.getId())) {
throw new IllegalArgumentException("Room already exists");
}
rooms.put(room.getId(), new RoomContainer(room, new AtomicInteger(0)));
}
}
@Override
public void remove(Room room) {
var nodeId = room.getNodeId();
var node = node2roomsMap.keySet().stream()
.filter(n -> room.getNodeId().equals(n.id()))
.findFirst();
synchronized (node2roomsMap) {
if (node.isEmpty()) {
throw new IllegalArgumentException("Node '" + nodeId + "' does not exist");
}
node2roomsMap.get(node.get()).remove(room.getId());
}
userRepository.onRemoveRoom(room);
}
@Override
public Optional<Room> find(Node node, String identifier) {
synchronized (node2roomsMap) {
if (!node2roomsMap.containsKey(node)) {
throw new IllegalArgumentException("Node '" + node.id() + "' does not exist");
}
var result = node2roomsMap.get(node).getOrDefault(identifier, null);
return result == null? Optional.empty() : Optional.of(result.room());
}
}
@Override
public List<Room> all(Node node) {
synchronized (node2roomsMap) {
return node2roomsMap.get(node).values().stream().map(RoomContainer::room).toList();
}
}
@Override
public int countAvailable(Node node, int requiredSlots) {
return (int) node2roomsMap.get(node).values().stream()
.filter(bucket -> bucket.isAvailable(requiredSlots))
.count();
}
@Override
public Optional<Room> pickFree(Node node, Collection<User> users) {
synchronized (node2roomsMap) {
if (!node2roomsMap.containsKey(node)) {
throw new IllegalArgumentException("Node '" + node.id() + "' does not exist");
}
var requiredSlots = users.size();
var container = node2roomsMap.get(node).values().stream() // TODO: pick room with used node balancing mode
.filter(r -> r.isAvailable(requiredSlots))
.findFirst();
if (container.isPresent()) {
var cont = container.get();
var addedUsers = userRepository.linkWithRoom(cont.room(), users, false);
cont.used().getAndAdd((int) addedUsers.values().stream().filter(Boolean.TRUE::equals).count());
}
return container.map(RoomContainer::room);
}
}
@Override
public void onCreateNode(Node node) {
synchronized (node2roomsMap) {
node2roomsMap.put(node, new Rooms());
}
}
@Override
public void onRemoveNode(Node node) {
synchronized (node2roomsMap) {
node2roomsMap.remove(node);
}
}
private record RoomContainer(Room room, AtomicInteger used) {
public boolean isAvailable(int requiredSlots) {
return room.isAvailable(used.get(), requiredSlots);
}
}
private static class Rooms extends LinkedHashMap<String, RoomContainer> {}
}

View File

@ -1,7 +1,7 @@
package ru.dragonestia.picker.repository.impl; package ru.dragonestia.picker.repository.impl;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.model.Bucket; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
@ -12,51 +12,51 @@ import java.util.concurrent.atomic.AtomicInteger;
@Repository @Repository
public class UserRepositoryImpl implements UserRepository { public class UserRepositoryImpl implements UserRepository {
private final Map<User, Set<Bucket>> usersMap = new ConcurrentHashMap<>(); private final Map<User, Set<Room>> usersMap = new ConcurrentHashMap<>();
private final Map<NodeBucketPath, Set<User>> bucketUsers = new ConcurrentHashMap<>(); private final Map<NodeRoomPath, Set<User>> roomUsers = new ConcurrentHashMap<>();
@Override @Override
public Map<User, Boolean> linkWithBucket(Bucket bucket, Collection<User> users, boolean force) { public Map<User, Boolean> linkWithRoom(Room room, Collection<User> users, boolean force) {
var result = new HashMap<User, Boolean>(); var result = new HashMap<User, Boolean>();
synchronized (usersMap) { synchronized (usersMap) {
var path = new NodeBucketPath(bucket.getNodeIdentifier(), bucket.getIdentifier()); var path = new NodeRoomPath(room.getNodeId(), room.getId());
var usersSet = bucketUsers.getOrDefault(path, new HashSet<>()); var usersSet = roomUsers.getOrDefault(path, new HashSet<>());
if (force || bucket.getSlots().isUnlimited()) { if (force || room.getSlots().isUnlimited()) {
users.forEach(user -> result.put(user, true)); users.forEach(user -> result.put(user, true));
} else { } else {
for (var user : users) { for (var user : users) {
var set = usersMap.getOrDefault(user, new HashSet<>()); var set = usersMap.getOrDefault(user, new HashSet<>());
result.put(user, !set.contains(bucket)); result.put(user, !set.contains(room));
} }
if (bucket.getSlots().getSlots() < usersSet.size() + users.size()) { if (room.getSlots().getSlots() < usersSet.size() + users.size()) {
throw new Error("Bucket are full"); throw new Error("Room are full");
} }
} }
for (var user: users) { for (var user: users) {
var set = usersMap.getOrDefault(user, new HashSet<>()); var set = usersMap.getOrDefault(user, new HashSet<>());
set.add(bucket); set.add(room);
usersMap.put(user, set); usersMap.put(user, set);
} }
usersSet.addAll(users); usersSet.addAll(users);
bucketUsers.put(path, usersSet); roomUsers.put(path, usersSet);
} }
return result; return result;
} }
@Override @Override
public int unlinkWithBucket(Bucket bucket, Collection<User> users) { public int unlinkWithRoom(Room room, Collection<User> users) {
var counter = new AtomicInteger(); var counter = new AtomicInteger();
synchronized (usersMap) { synchronized (usersMap) {
usersMap.forEach((user, set) -> { usersMap.forEach((user, set) -> {
if (!set.contains(bucket)) return; if (!set.contains(room)) return;
set.remove(bucket); set.remove(room);
counter.incrementAndGet(); counter.incrementAndGet();
if (set.isEmpty()) { if (set.isEmpty()) {
@ -64,30 +64,30 @@ public class UserRepositoryImpl implements UserRepository {
} }
}); });
var path = new NodeBucketPath(bucket.getNodeIdentifier(), bucket.getIdentifier()); var path = new NodeRoomPath(room.getNodeId(), room.getId());
var set = bucketUsers.getOrDefault(path, new HashSet<>()); var set = roomUsers.getOrDefault(path, new HashSet<>());
set.removeAll(users); set.removeAll(users);
if (set.isEmpty()) { if (set.isEmpty()) {
bucketUsers.remove(path); roomUsers.remove(path);
} else { } else {
bucketUsers.put(path, set); roomUsers.put(path, set);
} }
} }
return counter.get(); return counter.get();
} }
@Override @Override
public List<Bucket> findAllLinkedUserBuckets(User user) { public List<Room> findAllLinkedUserRooms(User user) {
synchronized (usersMap) { synchronized (usersMap) {
return usersMap.getOrDefault(user, new HashSet<>()).stream().toList(); return usersMap.getOrDefault(user, new HashSet<>()).stream().toList();
} }
} }
@Override @Override
public void onRemoveBucket(Bucket bucket) { public void onRemoveRoom(Room room) {
synchronized (usersMap) { synchronized (usersMap) {
usersMap.forEach((user, set) -> { usersMap.forEach((user, set) -> {
set.remove(bucket); set.remove(room);
if (set.isEmpty()) { if (set.isEmpty()) {
usersMap.remove(user); usersMap.remove(user);
} }
@ -96,15 +96,15 @@ public class UserRepositoryImpl implements UserRepository {
} }
@Override @Override
public List<User> usersOf(Bucket bucket) { public List<User> usersOf(Room room) {
synchronized (usersMap) { synchronized (usersMap) {
return bucketUsers.getOrDefault(new NodeBucketPath(bucket.getNodeIdentifier(), bucket.getIdentifier()), new HashSet<>()) return roomUsers.getOrDefault(new NodeRoomPath(room.getNodeId(), room.getId()), new HashSet<>())
.stream() .stream()
.toList(); .toList();
} }
} }
private record NodeBucketPath(String node, String bucket) { private record NodeRoomPath(String node, String bucket) {
@Override @Override
public int hashCode() { public int hashCode() {
@ -115,7 +115,7 @@ public class UserRepositoryImpl implements UserRepository {
public boolean equals(Object o) { public boolean equals(Object o) {
if (o == null) return false; if (o == null) return false;
if (o == this) return true; if (o == this) return true;
if (o instanceof NodeBucketPath other) { if (o instanceof NodeRoomPath other) {
return other.node().equals(node()) && other.bucket().equals(bucket()); return other.node().equals(node()) && other.bucket().equals(bucket());
} }
return false; return false;

View File

@ -1,27 +0,0 @@
package ru.dragonestia.picker.service;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import java.util.List;
import java.util.Optional;
public interface BucketService {
void createBucket(Bucket bucket);
void removeBucket(Bucket bucket);
Optional<Bucket> findBucket(Node node, String identifier);
List<Bucket> allBuckets(Node node);
default int countAvailableBuckets(Node node) {
return countAvailableBuckets(node, 1);
}
int countAvailableBuckets(Node node, int requiredSlots);
Bucket pickAvailableBucket(Node node, List<User> users);
}

View File

@ -7,11 +7,11 @@ import java.util.Optional;
public interface NodeService { public interface NodeService {
void createNode(Node node); void create(Node node);
void removeNode(Node node); void remove(Node node);
List<Node> allNodes(); List<Node> all();
Optional<Node> findNode(String identifier); Optional<Node> find(String nodeId);
} }

View File

@ -0,0 +1,27 @@
package ru.dragonestia.picker.service;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import java.util.List;
import java.util.Optional;
public interface RoomService {
void create(Room room);
void remove(Room room);
Optional<Room> find(Node node, String roomId);
List<Room> all(Node node);
default int countAvailable(Node node) {
return countAvailable(node, 1);
}
int countAvailable(Node node, int requiredSlots);
Room pickAvailable(Node node, List<User> users);
}

View File

@ -1,6 +1,6 @@
package ru.dragonestia.picker.service; package ru.dragonestia.picker.service;
import ru.dragonestia.picker.model.Bucket; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import java.util.Collection; import java.util.Collection;
@ -8,11 +8,11 @@ import java.util.List;
public interface UserService { public interface UserService {
List<Bucket> getUserBuckets(User user); List<Room> getUserRooms(User user);
void linkUsersWithBucket(Bucket bucket, Collection<User> users, boolean force); void linkUsersWithRoom(Room room, Collection<User> users, boolean force);
void unlinkUsersFromBucket(Bucket bucket, Collection<User> users); void unlinkUsersFromRoom(Room room, Collection<User> users);
List<User> getBucketUsers(Bucket bucket); List<User> getRoomUsers(Room room);
} }

View File

@ -1,54 +0,0 @@
package ru.dragonestia.picker.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.BucketRepository;
import ru.dragonestia.picker.service.BucketService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class BucketServiceImpl implements BucketService {
private final BucketRepository bucketRepository;
@Override
public void createBucket(Bucket bucket) {
if (!NamingValidator.validateBucketIdentifier(bucket.getIdentifier())) {
throw new Error("Invalid bucket identifier format");
}
bucketRepository.createBucket(bucket);
}
@Override
public void removeBucket(Bucket bucket) {
bucketRepository.removeBucket(bucket);
}
@Override
public Optional<Bucket> findBucket(Node node, String identifier) {
return bucketRepository.findBucket(node, identifier);
}
@Override
public List<Bucket> allBuckets(Node node) {
return bucketRepository.all(node);
}
@Override
public int countAvailableBuckets(Node node, int requiredSlots) {
throw new RuntimeException("Not implemented");
}
@Override
public Bucket pickAvailableBucket(Node node, List<User> users) {
throw new RuntimeException("Not implemented");
}
}

View File

@ -17,26 +17,26 @@ public class NodeServiceImpl implements NodeService {
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
@Override @Override
public void createNode(Node node) { public void create(Node node) {
if (!NamingValidator.validateNodeIdentifier(node.identifier())) { if (!NamingValidator.validateNodeId(node.id())) {
throw new Error("Invalid node identifier format"); throw new Error("Invalid node id format");
} }
nodeRepository.createNode(node); nodeRepository.create(node);
} }
@Override @Override
public void removeNode(Node node) { public void remove(Node node) {
nodeRepository.deleteNode(node); nodeRepository.delete(node);
} }
@Override @Override
public List<Node> allNodes() { public List<Node> all() {
return nodeRepository.all(); return nodeRepository.all();
} }
@Override @Override
public Optional<Node> findNode(String identifier) { public Optional<Node> find(String nodeId) {
return nodeRepository.findNode(identifier); return nodeRepository.find(nodeId);
} }
} }

View File

@ -0,0 +1,54 @@
package ru.dragonestia.picker.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class RoomServiceImpl implements RoomService {
private final RoomRepository roomRepository;
@Override
public void create(Room room) {
if (!NamingValidator.validateRoomId(room.getId())) {
throw new Error("Invalid room id format");
}
roomRepository.create(room);
}
@Override
public void remove(Room room) {
roomRepository.remove(room);
}
@Override
public Optional<Room> find(Node node, String roomId) {
return roomRepository.find(node, roomId);
}
@Override
public List<Room> all(Node node) {
return roomRepository.all(node);
}
@Override
public int countAvailable(Node node, int requiredSlots) {
throw new RuntimeException("Not implemented");
}
@Override
public Room pickAvailable(Node node, List<User> users) {
throw new RuntimeException("Not implemented");
}
}

View File

@ -2,7 +2,7 @@ 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.model.Bucket; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.service.UserService; import ru.dragonestia.picker.service.UserService;
@ -17,22 +17,22 @@ public class UserServiceImpl implements UserService {
private final UserRepository userRepository; private final UserRepository userRepository;
@Override @Override
public List<Bucket> getUserBuckets(User user) { public List<Room> getUserRooms(User user) {
return userRepository.findAllLinkedUserBuckets(user); return userRepository.findAllLinkedUserRooms(user);
} }
@Override @Override
public void linkUsersWithBucket(Bucket bucket, Collection<User> users, boolean force) { public void linkUsersWithRoom(Room room, Collection<User> users, boolean force) {
userRepository.linkWithBucket(bucket, users, force); userRepository.linkWithRoom(room, users, force);
} }
@Override @Override
public void unlinkUsersFromBucket(Bucket bucket, Collection<User> users) { public void unlinkUsersFromRoom(Room room, Collection<User> users) {
userRepository.unlinkWithBucket(bucket, users); userRepository.unlinkWithRoom(room, users);
} }
@Override @Override
public List<User> getBucketUsers(Bucket bucket) { public List<User> getRoomUsers(Room room) {
return userRepository.usersOf(bucket); return userRepository.usersOf(room);
} }
} }

View File

@ -5,15 +5,15 @@ import lombok.experimental.UtilityClass;
@UtilityClass @UtilityClass
public class NamingValidator { public class NamingValidator {
public boolean validateNodeIdentifier(String input) { public boolean validateNodeId(String input) {
return input.matches("^[a-z\\d-]+$"); return input.matches("^[a-z\\d-]+$");
} }
public boolean validateBucketIdentifier(String input) { public boolean validateRoomId(String input) {
return input.matches("^[a-z\\d-]+$"); return input.matches("^[a-z\\d-]+$");
} }
public boolean validateUserIdentifier(String input) { public boolean validateUserId(String input) {
return input.matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$"); return input.matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$");
} }
} }

View File

@ -1,39 +0,0 @@
package ru.dragonestia.picker.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import ru.dragonestia.picker.model.Bucket;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.type.LoadBalancingMethod;
import ru.dragonestia.picker.model.type.SlotLimit;
import java.util.HashMap;
public class BucketServiceTest {
private BucketService bucketService;
private HashMap<String, Bucket> bucketMap;
@BeforeEach
void setup() {
bucketMap = new HashMap<>();
bucketService = Mockito.mock(BucketService.class);
Mockito.doAnswer(invocation -> {
var bucket = invocation.getArgument(0, Bucket.class);
return null;
}).when(bucketService).createBucket(Mockito.any(Bucket.class));
}
Node createNode() {
return new Node("test-node", LoadBalancingMethod.ROUND_ROBIN);
}
@Test
void test() {
var node = createNode();
bucketService.createBucket(Bucket.create("test-bucket", node, SlotLimit.unlimited(), ""));
}
}

View File

@ -0,0 +1,39 @@
package ru.dragonestia.picker.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.model.type.SlotLimit;
import java.util.HashMap;
public class RoomServiceTest {
private RoomService roomService;
private HashMap<String, Room> roomMap;
@BeforeEach
void setup() {
roomMap = new HashMap<>();
roomService = Mockito.mock(RoomService.class);
Mockito.doAnswer(invocation -> {
var room = invocation.getArgument(0, Room.class);
return null;
}).when(roomService).create(Mockito.any(Room.class));
}
Node createNode() {
return new Node("test-node", PickingMode.ROUND_ROBIN);
}
@Test
void test() {
var node = createNode();
roomService.create(Room.create("test-room", node, SlotLimit.unlimited(), ""));
}
}

View File

@ -12,21 +12,21 @@ import com.vaadin.flow.component.icon.VaadinIcon;
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.Bucket; import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User; import ru.dragonestia.picker.cp.model.User;
import java.util.List; import java.util.List;
public class AddUsers extends Details { public class AddUsers extends Details {
private final Bucket bucket; private final Room room;
private final Checkbox ignoreSlots; private final Checkbox ignoreSlots;
private final VerticalLayout usersLayout; private final VerticalLayout usersLayout;
public AddUsers(Bucket bucket) { public AddUsers(Room room) {
super(new H2("Add users")); super(new H2("Add users"));
this.bucket = bucket; this.room = room;
usersLayout = new VerticalLayout(); usersLayout = new VerticalLayout();
add(addUserToTransacionButton()); add(addUserToTransacionButton());
@ -84,8 +84,8 @@ public class AddUsers extends Details {
} }
private TextField createUserIdentifierField(boolean canBeDeleted) { private TextField createUserIdentifierField(boolean canBeDeleted) {
var field = new TextField("User identifier"); var field = new TextField("User id");
field.setPlaceholder("example-user-identifier"); field.setPlaceholder("example-user-id");
field.setHelperText("It can be UUID, username, numeric ids, etc"); field.setHelperText("It can be UUID, username, numeric ids, etc");
field.setMinWidth(20, Unit.REM); field.setMinWidth(20, Unit.REM);

View File

@ -51,14 +51,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.identifier().startsWith(temp)) .filter(node -> node.id().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::identifier).setHeader("Identifier"); grid.addColumn(Node::id).setHeader("Identifier");
grid.addColumn(node -> node.method().getName()).setHeader("Mode"); grid.addColumn(node -> node.mode().getName()).setHeader("Mode");
grid.addComponentColumn(this::createManageButtons).setHeader("Manage"); grid.addComponentColumn(this::createManageButtons).setHeader("Manage");
return grid; return grid;
} }
@ -84,12 +84,12 @@ public class NodeList extends VerticalLayout {
} }
private void clickDetailsButton(Node node) { private void clickDetailsButton(Node node) {
getUI().ifPresent(ui -> ui.navigate("/nodes/" + node.identifier())); getUI().ifPresent(ui -> ui.navigate("/nodes/" + node.id()));
} }
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.identifier() + "' to field below and confirm.")); dialog.add(new Paragraph("Confirm that you want to delete node. Enter '" + node.id() + "' to field below and confirm."));
var inputField = new TextField(); var inputField = new TextField();
dialog.add(inputField); dialog.add(inputField);
@ -98,14 +98,14 @@ public class NodeList extends VerticalLayout {
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.identifier().equals(inputField.getValue())) { if (!node.id().equals(inputField.getValue())) {
Notification.show("Invalid input", 3000, Notification.Position.TOP_END) Notification.show("Invalid input", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR); .addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
removeNode(node); removeNode(node);
Notification.show("Node '" + node.identifier() + "' was successfully removed!", 3000, Notification.Position.TOP_END) Notification.show("Node '" + node.id() + "' was successfully removed!", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
dialog.close(); dialog.close();
}); });
@ -129,7 +129,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.identifier()); removeMethod.accept(node.id());
} }
} }
} }

View File

@ -17,7 +17,7 @@ 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.cp.model.Node;
import ru.dragonestia.picker.cp.model.type.LoadBalancingMethod; import ru.dragonestia.picker.cp.model.type.PickingMode;
import java.util.function.Function; import java.util.function.Function;
@ -25,7 +25,7 @@ public class RegisterNode extends Details {
private final Function<Node, Response> onSubmit; private final Function<Node, Response> onSubmit;
private final TextField identifierField; private final TextField identifierField;
private final RadioButtonGroup<LoadBalancingMethod> modeRadio; private final RadioButtonGroup<PickingMode> modeRadio;
public RegisterNode(Function<Node, Response> onSubmit) { public RegisterNode(Function<Node, Response> onSubmit) {
super(new H2("Register node")); super(new H2("Register node"));
@ -58,15 +58,15 @@ public class RegisterNode extends Details {
return button; return button;
} }
private RadioButtonGroup<LoadBalancingMethod> createModeRadio() { private RadioButtonGroup<PickingMode> createModeRadio() {
var radio = new RadioButtonGroup<LoadBalancingMethod>("Mode"); var radio = new RadioButtonGroup<PickingMode>("Mode");
radio.addThemeVariants(RadioGroupVariant.LUMO_VERTICAL); radio.addThemeVariants(RadioGroupVariant.LUMO_VERTICAL);
radio.setRenderer(new ComponentRenderer<Component, LoadBalancingMethod>(mode -> new Span(mode.getName()))); radio.setRenderer(new ComponentRenderer<Component, PickingMode>(mode -> new Span(mode.getName())));
radio.setItems(LoadBalancingMethod.SEQUENTIAL_FILLING, radio.setItems(PickingMode.SEQUENTIAL_FILLING,
LoadBalancingMethod.ROUND_ROBIN, PickingMode.ROUND_ROBIN,
LoadBalancingMethod.LEAST_PICKED); PickingMode.LEAST_PICKED);
radio.setValue(LoadBalancingMethod.SEQUENTIAL_FILLING); radio.setValue(PickingMode.SEQUENTIAL_FILLING);
return radio; return radio;
} }
@ -76,7 +76,7 @@ public class RegisterNode extends Details {
private @Nullable String validateForm(String identifier) { private @Nullable String validateForm(String identifier) {
if (identifier.isEmpty()) { if (identifier.isEmpty()) {
return "Node identifier cannot be empty"; return "Node id cannot be empty";
} }
return null; return null;
@ -88,7 +88,7 @@ public class RegisterNode extends Details {
String error = null; String error = null;
if (identifierField.isInvalid() || (error = validateForm(nodeIdentifier)) != null) { if (identifierField.isInvalid() || (error = validateForm(nodeIdentifier)) != null) {
if (identifierField.isInvalid()) { if (identifierField.isInvalid()) {
error = "Invalid node identifier format"; error = "Invalid node id format";
} }
Notification.show(error, 3000, Notification.Position.TOP_END) Notification.show(error, 3000, Notification.Position.TOP_END)

View File

@ -13,28 +13,28 @@ 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.Bucket; import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.type.SlotLimit; import ru.dragonestia.picker.cp.model.type.SlotLimit;
import java.util.function.Function; import java.util.function.Function;
public class RegisterBucket extends Details { public class RegisterRoom extends Details {
private final Node node; private final Node node;
private final Function<Bucket, Response> onSubmit; private final Function<Room, Response> onSubmit;
private final TextField identifierField; private final TextField identifierField;
private final TextArea payloadField; private final TextArea payloadField;
private final Checkbox lockedField; private final Checkbox lockedField;
public RegisterBucket(Node node, Function<Bucket, Response> onSubmit) { public RegisterRoom(Node node, Function<Room, Response> onSubmit) {
super(new H2("Register bucket")); super(new H2("Register room"));
this.node = node; this.node = node;
this.onSubmit = onSubmit; this.onSubmit = onSubmit;
var layout = new VerticalLayout(); var layout = new VerticalLayout();
layout.add(createNodeIdentifierField()); layout.add(createNodeIdentifierField());
layout.add(identifierField = createBucketIdentifierField()); layout.add(identifierField = createRoomIdentifierField());
layout.add(payloadField = createPayloadField()); layout.add(payloadField = createPayloadField());
layout.add(lockedField = createLockedField()); layout.add(lockedField = createLockedField());
layout.add(createSubmitButton()); layout.add(createSubmitButton());
@ -45,15 +45,15 @@ public class RegisterBucket 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.identifier()); field.setValue(node.id());
field.setReadOnly(true); field.setReadOnly(true);
return field; return field;
} }
private TextField createBucketIdentifierField() { private TextField createRoomIdentifierField() {
var field = new TextField("Identifier"); var field = new TextField("Identifier");
field.setMinWidth(20, Unit.REM); field.setMinWidth(20, Unit.REM);
field.setPlaceholder("example-bucket-id"); field.setPlaceholder("example-room-id");
field.setHelperText("The field can contain only lowercase letters, numbers and a dash character"); field.setHelperText("The field can contain only lowercase letters, numbers and a dash character");
field.setRequired(true); field.setRequired(true);
field.setPattern("^[a-z\\d-]+$"); field.setPattern("^[a-z\\d-]+$");
@ -103,7 +103,7 @@ public class RegisterBucket extends Details {
String error = null; String error = null;
if (identifierField.isInvalid() || (error = validateForm(nodeIdentifier)) != null) { if (identifierField.isInvalid() || (error = validateForm(nodeIdentifier)) != null) {
if (identifierField.isInvalid()) { if (identifierField.isInvalid()) {
error = "Invalid bucket identifier format"; error = "Invalid room id format";
} }
Notification.show(error, 3000, Notification.Position.TOP_END) Notification.show(error, 3000, Notification.Position.TOP_END)
@ -111,9 +111,9 @@ public class RegisterBucket extends Details {
return; return;
} }
var bucket = Bucket.create(nodeIdentifier, node, SlotLimit.unlimited(), payloadField.getValue()); var room = Room.create(nodeIdentifier, node, SlotLimit.unlimited(), payloadField.getValue());
bucket.setLocked(lockedField.getValue()); room.setLocked(lockedField.getValue());
var response = onSubmit.apply(bucket); var response = onSubmit.apply(room);
clear(); clear();
if (response.error()) { if (response.error()) {
Notification.show(response.reason(), 3000, Notification.Position.TOP_END) Notification.show(response.reason(), 3000, Notification.Position.TOP_END)
@ -121,7 +121,7 @@ public class RegisterBucket extends Details {
return; return;
} }
Notification.show("Bucket was successfully registered", 3000, Notification.Position.TOP_END) Notification.show("Room was successfully registered", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} }

View File

@ -16,32 +16,32 @@ 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.BucketDTO; import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
public class BucketList extends VerticalLayout { public class RoomList extends VerticalLayout {
private final String nodeIdentifier; private final String nodeIdentifier;
private final Grid<BucketDTO> bucketsGrid; private final Grid<RoomDTO> roomsGrid;
private final TextField searchField; private final TextField searchField;
private List<BucketDTO> cachedBuckets; private List<RoomDTO> cachedRooms;
@Setter private Consumer<BucketDTO> removeMethod; @Setter private Consumer<RoomDTO> removeMethod;
public BucketList(String nodeIdentifier, List<BucketDTO> buckets) { public RoomList(String nodeIdentifier, List<RoomDTO> buckets) {
this.nodeIdentifier = nodeIdentifier; this.nodeIdentifier = nodeIdentifier;
cachedBuckets = buckets; cachedRooms = buckets;
add(new H2("Buckets")); add(new H2("Rooms"));
add(searchField = createSearchField()); add(searchField = createSearchField());
add(bucketsGrid = createGrid()); add(roomsGrid = createGrid());
update(buckets); update(buckets);
} }
private TextField createSearchField() { private TextField createSearchField() {
var field = new TextField("Search bucket"); var field = new TextField("Search room");
field.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); field.setPrefixComponent(new Icon(VaadinIcon.SEARCH));
field.setClearButtonVisible(true); field.setClearButtonVisible(true);
field.setHelperText("Press Enter to search"); field.setHelperText("Press Enter to search");
@ -52,27 +52,27 @@ public class BucketList extends VerticalLayout {
private void applySearch(String input) { private void applySearch(String input) {
var temp = input.trim(); var temp = input.trim();
bucketsGrid.setItems(cachedBuckets.stream() roomsGrid.setItems(cachedRooms.stream()
.filter(bucket -> bucket.identifier().startsWith(temp)) .filter(room -> room.id().startsWith(temp))
.toList()); .toList());
} }
private Grid<BucketDTO> createGrid() { private Grid<RoomDTO> createGrid() {
var grid = new Grid<>(BucketDTO.class, false); var grid = new Grid<>(RoomDTO.class, false);
grid.addColumn(BucketDTO::identifier).setHeader("Identifier"); grid.addColumn(RoomDTO::id).setHeader("Identifier");
grid.addComponentColumn(bucket -> { grid.addComponentColumn(room -> {
var result = new Span(); var result = new Span();
if (bucket.slots() == -1) { if (room.slots() == -1) {
result.setText("Unlimited"); result.setText("Unlimited");
result.getElement().getThemeList().add("badge contrast"); result.getElement().getThemeList().add("badge contrast");
} else { } else {
result.setText(Integer.toString(bucket.slots())); result.setText(Integer.toString(room.slots()));
} }
return result; return result;
}).setHeader("Slots").setTextAlign(ColumnTextAlign.CENTER); }).setHeader("Slots").setTextAlign(ColumnTextAlign.CENTER);
grid.addComponentColumn(bucket -> { grid.addComponentColumn(room -> {
var result = new Span(); var result = new Span();
if (bucket.locked()) { if (room.locked()) {
result.setText("Yes"); result.setText("Yes");
result.getElement().getThemeList().add("badge error"); result.getElement().getThemeList().add("badge error");
} else { } else {
@ -84,19 +84,19 @@ public class BucketList extends VerticalLayout {
return grid; return grid;
} }
private HorizontalLayout createManageButtons(BucketDTO bucket) { private HorizontalLayout createManageButtons(RoomDTO room) {
var layout = new HorizontalLayout(); var layout = new HorizontalLayout();
{ {
var button = new Button("Details"); var button = new Button("Details");
button.addClickListener(event -> clickDetailsButton(bucket)); button.addClickListener(event -> clickDetailsButton(room));
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
layout.add(button); layout.add(button);
} }
{ {
var button = new Button("Remove"); var button = new Button("Remove");
button.addClickListener(event -> clickRemoveButton(bucket)); button.addClickListener(event -> clickRemoveButton(room));
button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); button.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR);
layout.add(button); layout.add(button);
} }
@ -104,16 +104,16 @@ public class BucketList extends VerticalLayout {
return layout; return layout;
} }
private void clickDetailsButton(BucketDTO bucket) { private void clickDetailsButton(RoomDTO bucket) {
getUI().ifPresent(ui -> { getUI().ifPresent(ui -> {
ui.navigate("/nodes/" + nodeIdentifier + ui.navigate("/nodes/" + nodeIdentifier +
"/buckets/" + bucket.identifier()); "/rooms/" + bucket.id());
}); });
} }
private void clickRemoveButton(BucketDTO bucket) { private void clickRemoveButton(RoomDTO 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.identifier() + "' to field below and confirm.")); dialog.add(new Paragraph("Confirm that you want to delete bucket. Enter '" + bucket.id() + "' to field below and confirm."));
var inputField = new TextField(); var inputField = new TextField();
dialog.add(inputField); dialog.add(inputField);
@ -122,14 +122,14 @@ public class BucketList extends VerticalLayout {
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 (!bucket.identifier().equals(inputField.getValue())) { if (!bucket.id().equals(inputField.getValue())) {
Notification.show("Invalid input", 3000, Notification.Position.TOP_END) Notification.show("Invalid input", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR); .addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
removeBucket(bucket); removeBucket(bucket);
Notification.show("Bucket '" + bucket.identifier() + "' was successfully removed!", 3000, Notification.Position.TOP_END) Notification.show("Bucket '" + bucket.id() + "' was successfully removed!", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
dialog.close(); dialog.close();
}); });
@ -146,12 +146,12 @@ public class BucketList extends VerticalLayout {
dialog.open(); dialog.open();
} }
public void update(List<BucketDTO> buckets) { public void update(List<RoomDTO> buckets) {
cachedBuckets = buckets; cachedRooms = buckets;
applySearch(searchField.getValue()); applySearch(searchField.getValue());
} }
private void removeBucket(BucketDTO bucket) { private void removeBucket(RoomDTO bucket) {
if (removeMethod != null) { if (removeMethod != null) {
removeMethod.accept(bucket); removeMethod.accept(bucket);
} }

View File

@ -4,7 +4,7 @@ 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.Bucket; import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User; import ru.dragonestia.picker.cp.model.User;
import java.util.ArrayList; import java.util.ArrayList;
@ -12,14 +12,14 @@ import java.util.List;
public class UserList extends VerticalLayout { public class UserList extends VerticalLayout {
private final Bucket bucket; private final Room room;
private final Grid<User> usersGrid; private final Grid<User> usersGrid;
private final Span totalUsers = new Span(); private final Span totalUsers = new Span();
private final Span occupancy = new Span(); private final Span occupancy = new Span();
private List<User> cachedUsers = new ArrayList<>(); private List<User> cachedUsers = new ArrayList<>();
public UserList(Bucket bucket, List<User> users) { public UserList(Room room, List<User> users) {
this.bucket = bucket; this.room = room;
add(usersGrid = createUsersGrid()); add(usersGrid = createUsersGrid());
@ -28,8 +28,8 @@ 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::identifier).setHeader("User Identifier").setFooter(totalUsers); grid.addColumn(User::id).setHeader("User Identifier").setFooter(totalUsers);
grid.addColumn(user -> 0).setTextAlign(ColumnTextAlign.CENTER).setHeader("Linked with buckets") // 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
return grid; return grid;
@ -39,6 +39,6 @@ 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(bucket.getUsingPercentage(users.size()))); occupancy.setText("Occupancy: %s".formatted(room.getUsingPercentage(users.size())));
} }
} }

View File

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

View File

@ -8,29 +8,30 @@ import ru.dragonestia.picker.cp.model.type.SlotLimit;
import java.net.URI; import java.net.URI;
@Getter @Getter
public class Bucket { public class Room {
private final String identifier; private final String id;
private final String nodeIdentifier; private final String nodeId;
private final SlotLimit slots; private final SlotLimit slots;
private final String payload; private final String payload;
private boolean locked = false; private boolean locked = false;
@JsonCreator @JsonCreator
private Bucket(@JsonProperty("identifier") String identifier, private Room(@JsonProperty("id") String id,
@JsonProperty("nodeIdentifier") String nodeIdentifier, @JsonProperty("nodeIdentifier") String nodeId,
@JsonProperty("slots") SlotLimit slots, @JsonProperty("slots") SlotLimit slots,
@JsonProperty("payload") String payload, @JsonProperty("payload") String payload,
@JsonProperty("locked") boolean locked) { @JsonProperty("locked") boolean locked) {
this.identifier = identifier;
this.nodeIdentifier = nodeIdentifier; this.id = id;
this.nodeId = nodeId;
this.slots = slots; this.slots = slots;
this.payload = payload; this.payload = payload;
this.locked = locked; this.locked = locked;
} }
public static Bucket create(String identifier, Node node, SlotLimit limit, String payload) { public static Room create(String roomId, Node node, SlotLimit limit, String payload) {
return new Bucket(identifier, node.identifier(), limit, payload, false); return new Room(roomId, node.id(), limit, payload, false);
} }
public void setLocked(boolean value) { public void setLocked(boolean value) {
@ -45,21 +46,21 @@ public class Bucket {
@Override @Override
public int hashCode() { public int hashCode() {
return identifier.hashCode(); return id.hashCode();
} }
@Override @Override
public boolean equals(Object object) { public boolean equals(Object object) {
if (object == this) return true; if (object == this) return true;
if (object == null) return false; if (object == null) return false;
if (object instanceof Bucket other) { if (object instanceof Room other) {
return identifier.equals(other.identifier); return id.equals(other.id);
} }
return false; return false;
} }
public URI createApiURI() { public URI createApiURI() {
return URI.create("/nodes/" + nodeIdentifier + "/buckets/" + identifier); return URI.create("/nodes/" + nodeId + "/rooms/" + id);
} }
public String getUsingPercentage(int used) { public String getUsingPercentage(int used) {

View File

@ -2,11 +2,11 @@ package ru.dragonestia.picker.cp.model;
import lombok.NonNull; import lombok.NonNull;
public record User(@NonNull String identifier) { public record User(@NonNull String id) {
@Override @Override
public int hashCode() { public int hashCode() {
return identifier.hashCode(); return id.hashCode();
} }
@Override @Override
@ -14,7 +14,7 @@ public record User(@NonNull String identifier) {
if (object == this) return true; if (object == this) return true;
if (object == null) return false; if (object == null) return false;
if (object instanceof User other) { if (object instanceof User other) {
return identifier.equals(other.identifier); return id.equals(other.id);
} }
return false; return false;
} }

View File

@ -4,13 +4,13 @@ import ru.dragonestia.picker.cp.model.Node;
import java.net.URI; import java.net.URI;
public record BucketDTO(String identifier, int slots, boolean locked) { public record RoomDTO(String id, int slots, boolean locked) {
public URI uriAPI(Node node) { public URI uriAPI(Node node) {
return uriAPI(node.identifier()); return uriAPI(node.id());
} }
public URI uriAPI(String nodeId) { public URI uriAPI(String nodeId) {
return URI.create("/nodes/" + nodeId + "/buckets/" + identifier); return URI.create("/nodes/" + nodeId + "/rooms/" + id);
} }
} }

View File

@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor;
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
public enum LoadBalancingMethod { 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");

View File

@ -13,12 +13,12 @@ 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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import ru.dragonestia.picker.cp.component.BucketList; 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.RegisterBucket; import ru.dragonestia.picker.cp.component.RegisterRoom;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.dto.BucketDTO; import ru.dragonestia.picker.cp.model.dto.RoomDTO;
import ru.dragonestia.picker.cp.repository.BucketRepository; import ru.dragonestia.picker.cp.repository.RoomRepository;
import ru.dragonestia.picker.cp.repository.NodeRepository; import ru.dragonestia.picker.cp.repository.NodeRepository;
import java.util.List; import java.util.List;
@ -29,16 +29,16 @@ import java.util.List;
public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserver { public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserver {
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
private final BucketRepository bucketRepository; private final RoomRepository roomRepository;
private Node node; private Node node;
private RegisterBucket registerBucket; private RegisterRoom registerRoom;
private BucketList bucketList; private RoomList roomList;
public NodeDetailsPage(@Autowired NodeRepository nodeRepository, public NodeDetailsPage(@Autowired NodeRepository nodeRepository,
@Autowired BucketRepository bucketRepository) { @Autowired RoomRepository roomRepository) {
this.nodeRepository = nodeRepository; this.nodeRepository = nodeRepository;
this.bucketRepository = bucketRepository; this.roomRepository = roomRepository;
} }
@Override @Override
@ -51,7 +51,7 @@ public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserv
var nodeId = nodeIdOpt.get(); var nodeId = nodeIdOpt.get();
add(new NavPath(new NavPath.Point("Nodes", "/nodes"), new NavPath.Point(nodeId, "/nodes/" + nodeId))); add(new NavPath(new NavPath.Point("Nodes", "/nodes"), new NavPath.Point(nodeId, "/nodes/" + nodeId)));
var nodeOpt = nodeRepository.findNode(nodeId); var nodeOpt = nodeRepository.find(nodeId);
if (nodeOpt.isEmpty()) { if (nodeOpt.isEmpty()) {
add(new H2("Error 404")); add(new H2("Error 404"));
add(new Paragraph("Node not found")); add(new Paragraph("Node not found"));
@ -61,27 +61,27 @@ public class NodeDetailsPage extends VerticalLayout implements BeforeEnterObserv
} }
node = nodeOpt.get(); node = nodeOpt.get();
initComponents(node, bucketRepository.all(node)); initComponents(node, roomRepository.all(node));
} }
private void initComponents(Node node, List<BucketDTO> buckets) { private void initComponents(Node node, List<RoomDTO> rooms) {
printNodeDetails(node); printNodeDetails(node);
add(new Hr()); add(new Hr());
add(registerBucket = new RegisterBucket(node, (bucket) -> { add(registerRoom = new RegisterRoom(node, (bucket) -> {
try { try {
bucketRepository.register(bucket); roomRepository.register(bucket);
return new RegisterBucket.Response(false, null); return new RegisterRoom.Response(false, null);
} catch (Error error) { } catch (Error error) {
return new RegisterBucket.Response(true, error.getMessage()); return new RegisterRoom.Response(true, error.getMessage());
} finally { } finally {
bucketList.update(bucketRepository.all(node)); roomList.update(roomRepository.all(node));
} }
})); }));
add(new Hr()); add(new Hr());
add(bucketList = new BucketList(node.identifier(), buckets)); add(roomList = new RoomList(node.id(), rooms));
bucketList.setRemoveMethod(bucket -> { roomList.setRemoveMethod(bucket -> {
bucketRepository.remove(node, bucket); roomRepository.remove(node, bucket);
bucketList.update(bucketRepository.all(node)); roomList.update(roomRepository.all(node));
}); });
} }
@ -89,8 +89,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.identifier() + "</b></span>")); layout.add(new Html("<span>Identifier: <b>" + node.id() + "</b></span>"));
layout.add(new Html("<span>Mode: <b>" + node.method().getName() + "</b></span>")); layout.add(new Html("<span>Mode: <b>" + node.mode().getName() + "</b></span>"));
add(layout); add(layout);
} }

View File

@ -31,15 +31,15 @@ public class NodesPage extends VerticalLayout {
add(new Hr()); add(new Hr());
add(nodeList = createNodeListElement()); add(nodeList = createNodeListElement());
nodeList.setRemoveMethod(nodeIdentifier -> { nodeList.setRemoveMethod(nodeIdentifier -> {
nodeRepository.removeNode(nodeIdentifier); nodeRepository.remove(nodeIdentifier);
nodeList.update(nodeRepository.getNodes()); nodeList.update(nodeRepository.all());
}); });
} }
protected RegisterNode createRegisterNodeElement() { protected RegisterNode createRegisterNodeElement() {
return new RegisterNode(node -> { return new RegisterNode(node -> {
try { try {
nodeRepository.registerNode(node); nodeRepository.register(node);
return new RegisterNode.Response(false, ""); return new RegisterNode.Response(false, "");
} catch (Error ex) { } catch (Error ex) {
return new RegisterNode.Response(true, ex.getMessage()); return new RegisterNode.Response(true, ex.getMessage());
@ -47,12 +47,12 @@ public class NodesPage extends VerticalLayout {
log.throwing(ex); log.throwing(ex);
return new RegisterNode.Response(true, ex.getMessage()); return new RegisterNode.Response(true, ex.getMessage());
} finally { } finally {
nodeList.update(nodeRepository.getNodes()); nodeList.update(nodeRepository.all());
} }
}); });
} }
protected NodeList createNodeListElement() { protected NodeList createNodeListElement() {
return new NodeList(nodeRepository.getNodes()); return new NodeList(nodeRepository.all());
} }
} }

View File

@ -1,6 +1,7 @@
package ru.dragonestia.picker.cp.page; package ru.dragonestia.picker.cp.page;
import com.vaadin.flow.component.Html; import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.Unit; import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.H2;
@ -19,29 +20,29 @@ import org.springframework.beans.factory.annotation.Autowired;
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.UserList; import ru.dragonestia.picker.cp.component.UserList;
import ru.dragonestia.picker.cp.model.Bucket; import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.Node; import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.repository.BucketRepository; import ru.dragonestia.picker.cp.repository.RoomRepository;
import ru.dragonestia.picker.cp.repository.NodeRepository; import ru.dragonestia.picker.cp.repository.NodeRepository;
import ru.dragonestia.picker.cp.repository.UserRepository; import ru.dragonestia.picker.cp.repository.UserRepository;
@Route("/nodes/:nodeId/buckets/:bucketId") @Route("/nodes/:nodeId/rooms/:roomId")
public class BucketDetailsPage extends VerticalLayout implements BeforeEnterObserver { public class RoomDetailsPage extends VerticalLayout implements BeforeEnterObserver {
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
private final BucketRepository bucketRepository; private final RoomRepository roomRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private Node node; private Node node;
private Bucket bucket; private Room room;
private AddUsers addUsers; private AddUsers addUsers;
private UserList userList; private UserList userList;
private Button lockBucketButton; private Button lockRoomButton;
private VerticalLayout bucketInfo; private VerticalLayout roomInfo;
@Autowired @Autowired
public BucketDetailsPage(NodeRepository nodeRepository, BucketRepository bucketRepository, UserRepository userRepository) { public RoomDetailsPage(NodeRepository nodeRepository, RoomRepository roomRepository, UserRepository userRepository) {
this.nodeRepository = nodeRepository; this.nodeRepository = nodeRepository;
this.bucketRepository = bucketRepository; this.roomRepository = roomRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
} }
@ -53,19 +54,19 @@ public class BucketDetailsPage extends VerticalLayout implements BeforeEnterObse
return; return;
} }
var bucketIdOpt = event.getRouteParameters().get("bucketId"); var roomIdOpt = event.getRouteParameters().get("roomId");
if (bucketIdOpt.isEmpty()) { if (roomIdOpt.isEmpty()) {
getUI().ifPresent(ui -> ui.navigate("/buckets/" + nodeIdOpt.get())); getUI().ifPresent(ui -> ui.navigate("/rooms/" + nodeIdOpt.get()));
return; return;
} }
var nodeId = nodeIdOpt.get(); var nodeId = nodeIdOpt.get();
var bucketId = bucketIdOpt.get(); var roomId = roomIdOpt.get();
add(new NavPath(new NavPath.Point("Nodes", "/nodes"), add(new NavPath(new NavPath.Point("Nodes", "/nodes"),
new NavPath.Point(nodeId, "/nodes/" + nodeId), new NavPath.Point(nodeId, "/nodes/" + nodeId),
new NavPath.Point(bucketId, "/nodes/" + nodeId + "/buckets/" + bucketId))); new NavPath.Point(roomId, "/nodes/" + nodeId + "/rooms/" + roomId)));
var nodeOpt = nodeRepository.findNode(nodeId); var nodeOpt = nodeRepository.find(nodeId);
if (nodeOpt.isEmpty()) { if (nodeOpt.isEmpty()) {
add(new H2("Error 404")); add(new H2("Error 404"));
add(new Paragraph("Node not found!")); add(new Paragraph("Node not found!"));
@ -75,75 +76,75 @@ public class BucketDetailsPage extends VerticalLayout implements BeforeEnterObse
} }
node = nodeOpt.get(); node = nodeOpt.get();
var bucketOpt = bucketRepository.find(node, bucketId); var bucketOpt = roomRepository.find(node, roomId);
if (bucketOpt.isEmpty()) { if (bucketOpt.isEmpty()) {
add(new H2("Error 404")); add(new H2("Error 404"));
add(new Paragraph("Bucket not found!")); add(new Paragraph("Room not found!"));
Notification.show("Bucket '" + nodeId + "' does not exist", 3000, Notification.Position.TOP_END) Notification.show("Room '" + nodeId + "' does not exist", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR); .addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
bucket = bucketOpt.get(); room = bucketOpt.get();
init(); init();
} }
private void init() { private void init() {
add(new H2("Bucket details")); add(new H2("Room details"));
printBucketDetails(); printRoomDetails();
add(new Hr()); add(new Hr());
add(addUsers = new AddUsers(bucket)); add(addUsers = new AddUsers(room));
add(new Hr()); add(new Hr());
add(new H2("Users")); add(new H2("Users"));
add(userList = new UserList(bucket, userRepository.all(bucket))); add(userList = new UserList(room, userRepository.all(room)));
} }
private void updateBucketInfo() { private void updateRoomInfo() {
bucketInfo.removeAll(); roomInfo.removeAll();
bucketInfo.add(new Html("<span>Node identifier: <b>" + bucket.getNodeIdentifier() + "</b></span>")); roomInfo.add(new Html("<span>Node identifier: <b>" + room.getNodeId() + "</b></span>"));
bucketInfo.add(new Html("<span>Bucket identifier: <b>" + bucket.getIdentifier() + "</b></span>")); roomInfo.add(new Html("<span>Room identifier: <b>" + room.getId() + "</b></span>"));
bucketInfo.add(new Html("<span>Slots: <b>" + (bucket.getSlots().isUnlimited()? "Unlimited" : bucket.getSlots().slots()) + "</b></span>")); roomInfo.add(new Html("<span>Slots: <b>" + (room.getSlots().isUnlimited()? "Unlimited" : room.getSlots().slots()) + "</b></span>"));
bucketInfo.add(new Html("<span>Locked: <b>" + (bucket.isLocked()? "Yes" : "No") + "</b></span>")); roomInfo.add(new Html("<span>Locked: <b>" + (room.isLocked()? "Yes" : "No") + "</b></span>"));
} }
private void printBucketDetails() { private void printRoomDetails() {
add(bucketInfo = new VerticalLayout()); add(roomInfo = new VerticalLayout());
bucketInfo.setPadding(false); roomInfo.setPadding(false);
updateBucketInfo(); updateRoomInfo();
add(lockBucketButton = new Button("", event -> changeBucketLockedState())); add(lockRoomButton = new Button("", event -> changeBucketLockedState()));
setLockBucketButtonState(); setLockRoomButtonState();
var payload = new TextArea("Payload(" + bucket.getPayload().length() + ")"); var payload = new TextArea("Payload(" + room.getPayload().length() + ")");
payload.setValue(bucket.getPayload()); payload.setValue(room.getPayload());
payload.setReadOnly(true); payload.setReadOnly(true);
payload.setMinWidth(50, Unit.REM); payload.setMinWidth(50, Unit.REM);
add(payload); add(payload);
} }
private void setLockBucketButtonState() { private void setLockRoomButtonState() {
if (bucket.isLocked()) { if (room.isLocked()) {
lockBucketButton.setText("Unlock"); lockRoomButton.setText("Unlock");
lockBucketButton.setPrefixComponent(new Icon(VaadinIcon.UNLOCK)); lockRoomButton.setPrefixComponent(new Icon(VaadinIcon.UNLOCK));
} else { } else {
lockBucketButton.setText("Lock"); lockRoomButton.setText("Lock");
lockBucketButton.setPrefixComponent(new Icon(VaadinIcon.LOCK)); lockRoomButton.setPrefixComponent(new Icon(VaadinIcon.LOCK));
} }
} }
private void changeBucketLockedState() { private void changeBucketLockedState() {
var newValue = !bucket.isLocked(); var newValue = !room.isLocked();
try { try {
bucketRepository.lock(bucket, newValue); roomRepository.lock(room, newValue);
} catch (Error error) { } catch (Error error) {
Notification.show(error.getMessage(), 3000, Notification.Position.TOP_END) Notification.show(error.getMessage(), 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_ERROR); .addThemeVariants(NotificationVariant.LUMO_ERROR);
return; return;
} }
bucket.setLocked(newValue); room.setLocked(newValue);
setLockBucketButtonState(); setLockRoomButtonState();
updateBucketInfo(); updateRoomInfo();
Notification.show("Success", 3000, Notification.Position.TOP_END) Notification.show("Success", 3000, Notification.Position.TOP_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS); .addThemeVariants(NotificationVariant.LUMO_SUCCESS);

View File

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

View File

@ -7,11 +7,11 @@ import java.util.Optional;
public interface NodeRepository { public interface NodeRepository {
void registerNode(Node node); void register(Node node);
List<Node> getNodes(); List<Node> all();
Optional<Node> findNode(String identifier); Optional<Node> find(String nodeId);
void removeNode(String identifier); void remove(String nodeId);
} }

View File

@ -0,0 +1,23 @@
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,6 +1,6 @@
package ru.dragonestia.picker.cp.repository; package ru.dragonestia.picker.cp.repository;
import ru.dragonestia.picker.cp.model.Bucket; import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User; import ru.dragonestia.picker.cp.model.User;
import java.util.Collection; import java.util.Collection;
@ -8,9 +8,9 @@ import java.util.List;
public interface UserRepository { public interface UserRepository {
void linkWithBucket(Bucket bucket, Collection<User> users); void linkWithRoom(Room room, Collection<User> users);
void unlinkFromBucket(Bucket bucket, Collection<User> users); void unlinkFromRoom(Room room, Collection<User> users);
List<User> all(Bucket bucket); List<User> all(Room room);
} }

View File

@ -1,100 +0,0 @@
package ru.dragonestia.picker.cp.repository.impl;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.client.HttpClientErrorException;
import ru.dragonestia.picker.cp.model.Bucket;
import ru.dragonestia.picker.cp.model.Node;
import ru.dragonestia.picker.cp.model.dto.BucketDTO;
import ru.dragonestia.picker.cp.repository.BucketRepository;
import ru.dragonestia.picker.cp.repository.impl.response.BucketInfoResponse;
import ru.dragonestia.picker.cp.repository.impl.response.BucketListResponse;
import ru.dragonestia.picker.cp.repository.impl.response.BucketRegisterResponse;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@Log4j2
@RequiredArgsConstructor
@SpringComponent
public class BucketRepositoryImpl implements BucketRepository {
private final RestUtil rest;
@Override
public List<BucketDTO> all(Node node) {
var entity = rest.getEntity(URI.create("/nodes/" + node.identifier() + "/buckets"),
BucketListResponse.class);
if (entity.getStatusCode().value() == 404) {
throw new Error("Node with identifier '" + node.identifier() + "' does not exists'");
}
if (!entity.hasBody()) {
throw new Error("Bucket list did not present");
}
return Objects.requireNonNull(entity.getBody()).buckets();
}
@Override
public void register(Bucket bucket) {
try {
var response = rest.post(URI.create("/nodes/" + bucket.getNodeIdentifier() + "/buckets"),
BucketRegisterResponse.class,
params -> {
params.put("identifier", bucket.getIdentifier());
params.put("slots", Integer.toString(bucket.getSlots().slots()));
params.put("payload", bucket.getPayload());
params.put("locked", Boolean.toString(bucket.isLocked()));
});
if (response.success()) return;
throw new Error(response.message());
} catch (HttpClientErrorException ex) {
var response = ex.getResponseBodyAs(BucketRegisterResponse.class);
if (response != null) {
throw new Error(response.message());
}
log.throwing(ex);
throw new Error("Internal error. Check logs");
}
}
@Override
public void remove(Bucket bucket) {
rest.delete(URI.create("/nodes/" + bucket.getNodeIdentifier() + "/buckets/" + bucket.getIdentifier()), params -> {});
}
@Override
public void remove(Node node, BucketDTO bucket) {
rest.delete(URI.create("/nodes/" + node.identifier() + "/buckets/" + bucket.identifier()), params -> {});
}
@Override
public Optional<Bucket> find(Node node, String identifier) {
try {
var response = rest.get(URI.create("/nodes/" + node.identifier() + "/buckets/" + identifier), BucketInfoResponse.class, map -> {});
return Optional.of(response.bucket());
} catch (Exception ex) {
return Optional.empty();
}
}
@Override
public void lock(Bucket bucket, boolean value) {
try {
rest.post(URI.create(bucket.createApiURI() + "/lock"), Boolean.class, params -> {
params.put("state", Boolean.toString(value));
});
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Error when changing bucket locked state");
}
}
}

View File

@ -21,14 +21,14 @@ public class NodeRepositoryImpl implements NodeRepository {
private final RestUtil rest; private final RestUtil rest;
@Override @Override
public void registerNode(Node node) { public void register(Node node) {
NodeRegisterResponse response; NodeRegisterResponse response;
try { try {
response = rest.post(URI.create("nodes"), response = rest.post(URI.create("nodes"),
NodeRegisterResponse.class, NodeRegisterResponse.class,
params -> { params -> {
params.put("identifier", node.identifier()); params.put("nodeId", node.id());
params.put("method", node.method().name()); params.put("method", node.mode().name());
}); });
} catch (Exception ex) { } catch (Exception ex) {
throw new RuntimeException("Internal error", ex); throw new RuntimeException("Internal error", ex);
@ -40,14 +40,14 @@ public class NodeRepositoryImpl implements NodeRepository {
} }
@Override @Override
public List<Node> getNodes() { public List<Node> all() {
return rest.get(URI.create("nodes"), NodeListResponse.class).nodes(); return rest.get(URI.create("nodes"), NodeListResponse.class).nodes();
} }
@Override @Override
public Optional<Node> findNode(String identifier) { public Optional<Node> find(String nodeId) {
try { try {
var response = rest.get(URI.create("nodes/" + identifier), NodeDetailsResponse.class); var response = rest.get(URI.create("nodes/" + nodeId), NodeDetailsResponse.class);
return Optional.of(response.node()); return Optional.of(response.node());
} catch (Exception ex) { } catch (Exception ex) {
return Optional.empty(); return Optional.empty();
@ -55,7 +55,7 @@ public class NodeRepositoryImpl implements NodeRepository {
} }
@Override @Override
public void removeNode(String identifier) { public void remove(String nodeId) {
rest.delete(URI.create("nodes/" + identifier), params -> {}); rest.delete(URI.create("nodes/" + nodeId), params -> {});
} }
} }

View File

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

View File

@ -3,10 +3,10 @@ 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.Bucket; import ru.dragonestia.picker.cp.model.Room;
import ru.dragonestia.picker.cp.model.User; import ru.dragonestia.picker.cp.model.User;
import ru.dragonestia.picker.cp.repository.UserRepository; import ru.dragonestia.picker.cp.repository.UserRepository;
import ru.dragonestia.picker.cp.repository.impl.response.BucketUserListResponse; import ru.dragonestia.picker.cp.repository.impl.response.RoomUserListResponse;
import java.net.URI; import java.net.URI;
import java.util.Collection; import java.util.Collection;
@ -20,20 +20,20 @@ public class UserRepositoryImpl implements UserRepository {
private final RestUtil rest; private final RestUtil rest;
@Override @Override
public void linkWithBucket(Bucket bucket, Collection<User> users) { public void linkWithRoom(Room room, Collection<User> users) {
// TODO // TODO
} }
@Override @Override
public void unlinkFromBucket(Bucket bucket, Collection<User> users) { public void unlinkFromRoom(Room room, Collection<User> users) {
// TODO // TODO
} }
@Override @Override
public List<User> all(Bucket bucket) { public List<User> all(Room room) {
try { try {
var response = rest.get(URI.create("/nodes/%s/buckets/%s/users".formatted(bucket.getNodeIdentifier(), bucket.getIdentifier())), var response = rest.get(URI.create("/nodes/%s/rooms/%s/users".formatted(room.getNodeId(), room.getId())),
BucketUserListResponse.class, RoomUserListResponse.class,
params -> {}); params -> {});
return response.users(); return response.users();

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
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) {}

View File

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

View File

@ -4,4 +4,4 @@ import ru.dragonestia.picker.cp.model.User;
import java.util.List; import java.util.List;
public record BucketUserListResponse(int slots, int usedSlots, List<User> users) {} public record RoomUserListResponse(int slots, int usedSlots, List<User> users) {}