diff --git a/service-chats/pom.xml b/service-chats/pom.xml
index 9ecd039..16e10cf 100644
--- a/service-chats/pom.xml
+++ b/service-chats/pom.xml
@@ -16,8 +16,6 @@
io.quarkus.platform
2.4.1.Final
3.0.0-M5
- 2.7.9.2
- 4.0.183
@@ -87,18 +85,10 @@
rest-assured
test
-
- it.tdlight
- tdlight-java
-
-
- it.tdlight
- tdlight-natives-linux-amd64
-
-
- org.apache.commons
- commons-lang3
+ org.assertj
+ assertj-core
+ test
diff --git a/service-chats/src/main/java/io/volvox/chats/Chat.java b/service-chats/src/main/java/io/volvox/chats/Chat.java
index 650fe51..d07174f 100644
--- a/service-chats/src/main/java/io/volvox/chats/Chat.java
+++ b/service-chats/src/main/java/io/volvox/chats/Chat.java
@@ -1,23 +1,58 @@
package io.volvox.chats;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
+import java.util.StringJoiner;
+import javax.persistence.Cacheable;
+import javax.persistence.Column;
import javax.persistence.Entity;
+import javax.persistence.Enumerated;
import javax.persistence.Id;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Positive;
+import org.hibernate.validator.constraints.Length;
@Entity
+@Cacheable
public class Chat extends PanacheEntityBase {
- @Id
- public String id;
+ @Id
+ @Positive(message = "id is not positive")
+ @Max(message = "id is too big", value = ChatId.MASK)
+ @Column(nullable = false, unique = true)
+ @JsonSerialize(using = ChatIdJsonSerializer.class)
+ @JsonDeserialize(using = ChatIdJsonDeserializer.class)
+ public Long id;
+ @Column(length = 128)
public String name;
+ @Length(message = "Username length is not valid", min = 5)
+ @Column(length = 48)
+ @Length(message = "Username must not be an empty string", min = 1, max = 12 + 32)
+ @Pattern(message = "Username contains invalid characters", regexp = "^(?:[a-zA-Z\\d][_]?)+$")
+ @Pattern(message = "Username is not valid", regexp = "^(?:translation_|mv_)?[a-zA-Z]([a-zA-Z_\\d]){1,30}[a-zA-Z\\d]$")
public String username;
+ @Enumerated
public Status status;
+ @JsonIgnore
public ChatId getChatId() {
- return new ChatId(id);
+ return ChatId.fromLong(id);
}
+ @JsonIgnore
public void setChatId(ChatId id) {
- this.id = id.toString();
+ this.id = id.toLong();
+ }
+
+ @Override public String toString() {
+ return new StringJoiner(", ", Chat.class.getSimpleName() + "[", "]")
+ .add("id=" + id)
+ .add("name='" + name + "'")
+ .add("username='" + username + "'")
+ .add("status=" + status)
+ .toString();
}
}
diff --git a/service-chats/src/main/java/io/volvox/chats/ChatId.java b/service-chats/src/main/java/io/volvox/chats/ChatId.java
index 9141d4d..52a9bf2 100644
--- a/service-chats/src/main/java/io/volvox/chats/ChatId.java
+++ b/service-chats/src/main/java/io/volvox/chats/ChatId.java
@@ -1,35 +1,116 @@
package io.volvox.chats;
-public record ChatId(Type type, long id) {
- ChatId(String id) {
- this(getType(id), getIdLong(id));
+public record ChatId(Type type, long subId) {
+
+ public static final int SUB_ID_MASK_BYTES = 52;
+ public static final int TYPE_MASK_BYTES = 2;
+
+ public static final long SUB_ID_MASK = 0b001111111111111111111111111111111111111111111111111111L;
+ public static final long TYPE_MASK = 0b11L << SUB_ID_MASK_BYTES;
+ public static final long MASK = SUB_ID_MASK | TYPE_MASK;
+ public static final int TYPE_PRIVATE_INT = 0b00;
+ public static final int TYPE_BASIC_INT = 0b01;
+ public static final int TYPE_SUPER_INT = 0b10;
+ public static final int TYPE_SECRET_INT = 0b11;
+ public static final long TYPE_PRIVATE_LONG = 0;
+ public static final long TYPE_BASIC_LONG = 0b01L << SUB_ID_MASK_BYTES & TYPE_MASK;
+ public static final long TYPE_SUPER_LONG = 0b10L << SUB_ID_MASK_BYTES & TYPE_MASK;
+ public static final long TYPE_SECRET_LONG = 0b11L << SUB_ID_MASK_BYTES & TYPE_MASK;
+
+ public ChatId {
+ if ((subId & SUB_ID_MASK) != subId) {
+ throw new IllegalArgumentException("subId is too big");
+ }
}
- private static Type getType(String id) {
- return switch (id.charAt(0)) {
- case 's' -> Type.SUPER;
- case 'b' -> Type.BASIC;
- case 'u' -> Type.PRIVATE;
- default -> throw new IllegalArgumentException();
+ public static ChatId fromLong(long id) {
+ return new ChatId(getType(id), getIdLong(id));
+ }
+
+ private static Type getType(long id) {
+ return switch ((int) ((id & TYPE_MASK) >> SUB_ID_MASK_BYTES)) {
+ case TYPE_SUPER_INT -> Type.SUPER;
+ case TYPE_BASIC_INT -> Type.BASIC;
+ case TYPE_PRIVATE_INT -> Type.PRIVATE;
+ case TYPE_SECRET_INT -> Type.SECRET;
+ default -> throw new IllegalArgumentException("Invalid id type: " + id);
};
}
- private static long getIdLong(String id) {
- return Long.parseUnsignedLong(id.substring(1));
+ private static long getIdLong(long id) {
+ return id & SUB_ID_MASK;
+ }
+
+ public long toLong() {
+ return switch (type) {
+ case SUPER -> TYPE_SUPER_LONG;
+ case BASIC -> TYPE_BASIC_LONG;
+ case PRIVATE -> TYPE_PRIVATE_LONG;
+ case SECRET -> TYPE_SECRET_LONG;
+ } | (subId & SUB_ID_MASK);
}
public enum Type {
+ PRIVATE,
BASIC,
SUPER,
- PRIVATE
+ SECRET
}
@Override
public String toString() {
- return switch (type) {
+ return toString(this);
+ }
+
+ public static String toString(ChatId chatId) {
+ return Long.toUnsignedString(chatId.subId) + "-" + switch (chatId.type) {
case SUPER -> 's';
case BASIC -> 'b';
case PRIVATE -> 'u';
- } + Long.toUnsignedString(id);
+ case SECRET -> 'd';
+ };
+ }
+
+ public static String toString(long chatId) {
+ return Long.toUnsignedString(getIdLong(chatId)) + "-" + switch (getType(chatId)) {
+ case SUPER -> 's';
+ case BASIC -> 'b';
+ case PRIVATE -> 'u';
+ case SECRET -> 'd';
+ };
+ }
+
+ public static ChatId fromString(String chatId) {
+ var parts = chatId.split("-", 2);
+ if (parts.length != 2) {
+ throw new IllegalArgumentException("Malformed chat id");
+ }
+ if (parts[1].length() != 1) {
+ throw new IllegalArgumentException("Chat type is too long");
+ }
+ return new ChatId(switch(parts[1].charAt(0)) {
+ case 's' -> Type.SUPER;
+ case 'b' -> Type.BASIC;
+ case 'u' -> Type.PRIVATE;
+ case 'd' -> Type.SECRET;
+ default -> throw new IllegalStateException("Unexpected value: " + parts[1].charAt(0));
+ }, Long.parseUnsignedLong(parts[0]) & SUB_ID_MASK);
+ }
+
+ public static Long stringToLong(String chatId) {
+ var parts = chatId.split("-", 2);
+ if (parts.length != 2) {
+ throw new IllegalArgumentException("Malformed chat id");
+ }
+ if (parts[1].length() != 1) {
+ throw new IllegalArgumentException("Chat type is too long");
+ }
+ return switch(parts[1].charAt(0)) {
+ case 's' -> TYPE_SUPER_LONG;
+ case 'b' -> TYPE_BASIC_LONG;
+ case 'u' -> TYPE_PRIVATE_LONG;
+ case 'd' -> TYPE_SECRET_LONG;
+ default -> throw new IllegalStateException("Unexpected value: " + parts[1].charAt(0));
+ } | (Long.parseUnsignedLong(parts[0]) & SUB_ID_MASK);
}
}
diff --git a/service-chats/src/main/java/io/volvox/chats/ChatIdJsonDeserializer.java b/service-chats/src/main/java/io/volvox/chats/ChatIdJsonDeserializer.java
new file mode 100644
index 0000000..3cb146d
--- /dev/null
+++ b/service-chats/src/main/java/io/volvox/chats/ChatIdJsonDeserializer.java
@@ -0,0 +1,15 @@
+package io.volvox.chats;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import java.io.IOException;
+
+public class ChatIdJsonDeserializer extends JsonDeserializer {
+
+ @Override
+ public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ var id = p.readValueAs(String.class);
+ return ChatId.stringToLong(id);
+ }
+}
diff --git a/service-chats/src/main/java/io/volvox/chats/ChatIdJsonSerializer.java b/service-chats/src/main/java/io/volvox/chats/ChatIdJsonSerializer.java
new file mode 100644
index 0000000..a07cd43
--- /dev/null
+++ b/service-chats/src/main/java/io/volvox/chats/ChatIdJsonSerializer.java
@@ -0,0 +1,14 @@
+package io.volvox.chats;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import java.io.IOException;
+
+public class ChatIdJsonSerializer extends JsonSerializer {
+
+ @Override
+ public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ gen.writeString(ChatId.toString(value));
+ }
+}
diff --git a/service-chats/src/main/java/io/volvox/chats/ChatRepository.java b/service-chats/src/main/java/io/volvox/chats/ChatRepository.java
index 69d6f33..14dd399 100644
--- a/service-chats/src/main/java/io/volvox/chats/ChatRepository.java
+++ b/service-chats/src/main/java/io/volvox/chats/ChatRepository.java
@@ -14,7 +14,7 @@ import org.hibernate.annotations.NamedQuery;
@NamedQuery(name = "Chat.updateStatusById", query = "update Chat p set p.status = :status where p.id = :id"),
@NamedQuery(name = "Chat.deleteById", query = "delete from Chat p where p.id = ?1")
})
-public class ChatRepository implements PanacheRepositoryBase {
+public class ChatRepository implements PanacheRepositoryBase {
public Uni findByUsername(String username) {
return find("#Chat.getByUsername", username).firstResult();
diff --git a/service-chats/src/main/java/io/volvox/chats/ChatResource.java b/service-chats/src/main/java/io/volvox/chats/ChatResource.java
index c9f5469..4cd1454 100644
--- a/service-chats/src/main/java/io/volvox/chats/ChatResource.java
+++ b/service-chats/src/main/java/io/volvox/chats/ChatResource.java
@@ -1,11 +1,10 @@
package io.volvox.chats;
-import io.quarkus.hibernate.reactive.panache.PanacheEntityBase;
+import io.quarkus.hibernate.reactive.panache.Panache;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import java.net.URI;
import javax.inject.Inject;
-import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
@@ -33,24 +32,21 @@ public class ChatResource {
@GET
@Path("/{id}")
- public Uni get(@PathParam("id") String id) {
+ public Uni get(@PathParam("id") Long id) {
return chatRepository.findById(id);
}
@POST
- @Transactional
public Uni create(Chat chat) {
- return chatRepository.persist(chat)
- // Return success
- .replaceWith(() -> Response.created(URI.create("/api/" + chat.id)).build());
+ return Panache.withTransaction(() -> chatRepository.persist(chat))
+ .onItem().transform(inserted -> Response.created(URI.create("/api/" + chat.id)).build());
}
@PUT
@Path("/{id}")
- @Transactional
- public Uni update(@PathParam("id") String id, Chat chat) {
+ public Uni update(@PathParam("id") Long id, Chat chat) {
// Find chat by id
- return chatRepository.findById(id)
+ return Panache.withTransaction(() -> chatRepository.findById(id)
.flatMap(entity -> {
if (entity == null) {
// Persist the chat if not found
@@ -61,16 +57,15 @@ public class ChatResource {
// Return the updated item
return Uni.createFrom().item(entity);
}
- });
+ }));
}
@DELETE
@Path("/{id}")
- @Transactional
- public Uni delete(@PathParam("id") String id) {
- return chatRepository.findById(id)
+ public Uni delete(@PathParam("id") Long id) {
+ return Panache.withTransaction(() -> chatRepository.findById(id)
.onItem().ifNull().failWith(NotFoundException::new)
- .flatMap(PanacheEntityBase::delete);
+ .flatMap(chatRepository::delete));
}
@GET
diff --git a/service-chats/src/main/java/io/volvox/chats/ChatsService.java b/service-chats/src/main/java/io/volvox/chats/ChatsService.java
index a00875d..330a32b 100644
--- a/service-chats/src/main/java/io/volvox/chats/ChatsService.java
+++ b/service-chats/src/main/java/io/volvox/chats/ChatsService.java
@@ -21,7 +21,7 @@ public class ChatsService {
}
@ConsumeEvent(value = "chats.get")
- public void get(Message msg) {
+ public void get(Message msg) {
chatResource.get(msg.body()).subscribe().with(msg::reply);
}
diff --git a/service-chats/src/main/java/io/volvox/chats/Status.java b/service-chats/src/main/java/io/volvox/chats/Status.java
index b95809e..f5220b4 100644
--- a/service-chats/src/main/java/io/volvox/chats/Status.java
+++ b/service-chats/src/main/java/io/volvox/chats/Status.java
@@ -1,7 +1,6 @@
package io.volvox.chats;
public enum Status {
- ALIVE,
- DELETED,
- UNKNOWN
+ DEAD,
+ ALIVE
}
diff --git a/service-chats/src/main/resources/application.properties b/service-chats/src/main/resources/application.properties
index e69de29..7b73292 100644
--- a/service-chats/src/main/resources/application.properties
+++ b/service-chats/src/main/resources/application.properties
@@ -0,0 +1,10 @@
+%prod.quarkus.datasource.db-kind=postgresql
+%prod.quarkus.datasource.username=quarkus_test
+%prod.quarkus.datasource.password=quarkus_test
+
+quarkus.hibernate-orm.database.generation=drop-and-create
+quarkus.hibernate-orm.log.sql=true
+quarkus.hibernate-orm.sql-load-script=import.sql
+
+# Reactive config
+%prod.quarkus.datasource.reactive.url=vertx-reactive:postgresql://localhost/quarkus_test
diff --git a/service-chats/src/main/resources/import.sql b/service-chats/src/main/resources/import.sql
new file mode 100644
index 0000000..fb6d842
--- /dev/null
+++ b/service-chats/src/main/resources/import.sql
@@ -0,0 +1,4 @@
+INSERT INTO Chat(id, name, username, status) VALUES (9007199256673076, 'My Supergroup', 'mysupergroup', 1);
+INSERT INTO Chat(id, name, username, status) VALUES (777000, 'Telegram', 'telegram', null);
+INSERT INTO Chat(id, name, username, status) VALUES (4503599627464345, 'School group', null, 1);
+INSERT INTO Chat(id, name, username, status) VALUES (4503599627382355, 'Old school group', null, 0);
diff --git a/service-chats/src/test/java/io/volvox/chats/ChatIdTest.java b/service-chats/src/test/java/io/volvox/chats/ChatIdTest.java
new file mode 100644
index 0000000..e387c48
--- /dev/null
+++ b/service-chats/src/test/java/io/volvox/chats/ChatIdTest.java
@@ -0,0 +1,70 @@
+package io.volvox.chats;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import io.volvox.chats.ChatId.Type;
+import org.junit.jupiter.api.Test;
+
+public class ChatIdTest {
+
+ @Test
+ public void newChatId() {
+ assertEquals(Type.BASIC, new ChatId(Type.BASIC, 10).type());
+ assertEquals(Type.PRIVATE, new ChatId(Type.PRIVATE, 10).type());
+ assertEquals(Type.SECRET, new ChatId(Type.SECRET, 10).type());
+ assertEquals(Type.SUPER, new ChatId(Type.SUPER, 10).type());
+
+ assertEquals(1, new ChatId(Type.BASIC, 1).subId());
+ assertEquals(2, new ChatId(Type.PRIVATE, 2).subId());
+ assertEquals(3, new ChatId(Type.SECRET, 3).subId());
+ assertEquals(4, new ChatId(Type.SUPER, 4).subId());
+ }
+
+ @Test
+ public void toLong() {
+ assertEquals(777000, new ChatId(Type.PRIVATE, 777000).toLong());
+ assertEquals(0b01L << 52 | 777000, new ChatId(Type.BASIC, 777000).toLong());
+ assertEquals(0b10L << 52 | 777000, new ChatId(Type.SUPER, 777000).toLong());
+ assertEquals(0b11L << 52 | 777000, new ChatId(Type.SECRET, 777000).toLong());
+ }
+
+ @Test
+ public void fromLong() {
+ assertEquals(new ChatId(Type.PRIVATE, 777000), ChatId.fromLong(777000));
+ assertEquals(new ChatId(Type.BASIC, 777000), ChatId.fromLong(0b01L << 52 | 777000));
+ assertEquals(new ChatId(Type.SUPER, 777000), ChatId.fromLong(0b10L << 52 | 777000));
+ assertEquals(new ChatId(Type.SECRET, 777000), ChatId.fromLong(0b11L << 52 | 777000));
+ }
+
+ @Test
+ public void fromString() {
+ assertEquals(new ChatId(Type.PRIVATE, 777000), ChatId.fromString("777000-u"));
+ assertEquals(new ChatId(Type.BASIC, 777000), ChatId.fromString("777000-b"));
+ assertEquals(new ChatId(Type.SUPER, 777000), ChatId.fromString("777000-s"));
+ assertEquals(new ChatId(Type.SECRET, 777000), ChatId.fromString("777000-d"));
+ }
+
+ @Test
+ public void testToString() {
+ assertEquals("777000-u", new ChatId(Type.PRIVATE, 777000).toString());
+ assertEquals("777000-b", new ChatId(Type.BASIC, 777000).toString());
+ assertEquals("777000-s", new ChatId(Type.SUPER, 777000).toString());
+ assertEquals("777000-d", new ChatId(Type.SECRET, 777000).toString());
+ }
+
+ @Test
+ public void longToString() {
+ assertEquals("777000-u", ChatId.toString(777000));
+ assertEquals("777000-b", ChatId.toString(0b01L << 52 | 777000));
+ assertEquals("777000-s", ChatId.toString(0b10L << 52 | 777000));
+ assertEquals("777000-d", ChatId.toString(0b11L << 52 | 777000));
+ }
+
+ @Test
+ public void stringToLong() {
+ assertEquals(777000, ChatId.stringToLong("777000-u"));
+ assertEquals(0b01L << 52 | 777000, ChatId.stringToLong("777000-b"));
+ assertEquals(0b10L << 52 | 777000, ChatId.stringToLong("777000-s"));
+ assertEquals(0b11L << 52 | 777000, ChatId.stringToLong("777000-d"));
+ }
+}
diff --git a/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java b/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java
new file mode 100644
index 0000000..7d552c5
--- /dev/null
+++ b/service-chats/src/test/java/io/volvox/chats/ChatsEndpointTest.java
@@ -0,0 +1,130 @@
+package io.volvox.chats;
+
+import static io.restassured.RestAssured.given;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.Matchers.emptyString;
+
+import io.quarkus.hibernate.reactive.panache.Panache;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.response.Response;
+import javax.inject.Inject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+public class ChatsEndpointTest {
+
+ @Inject
+ ChatRepository chatRepository;
+
+ @Test
+ public void testListAllChats() {
+ //List all, should have all 3 usernames the database has initially:
+ Response response = given()
+ .when()
+ .get("/api/chats")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .extract().response();
+ assertThat(response.jsonPath().getList("name")).containsExactlyInAnyOrder("My Supergroup", "Telegram", "School group", "Old school group");
+
+ // Update Telegram to Telegram Official
+ given()
+ .when()
+ .body("{\"name\" : \"Telegram Official\"}")
+ .contentType("application/json")
+ .put("/api/chats/777000")
+ .then()
+ .statusCode(200)
+ .body(
+ containsString("\"id\":"),
+ containsString("\"name\":\"Telegram Official\""));
+
+ //List all, Telegram Official should've replaced Telegram:
+ response = given()
+ .when()
+ .get("/api/chats")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .extract().response();
+ assertThat(response.jsonPath().getList("name"))
+ .containsExactlyInAnyOrder("My Supergroup", "Telegram Official", "School group", "Old school group");
+
+ //Delete Telegram:
+ given()
+ .when()
+ .delete("/api/chats/777000")
+ .then()
+ .statusCode(204);
+
+ response = given()
+ .when()
+ .get("/api/chats")
+ .then()
+ .statusCode(200)
+ .contentType("application/json")
+ .extract().response();
+ assertThat(response.jsonPath().getList("name"))
+ .containsExactlyInAnyOrder("My Supergroup", "School group", "Old school group");
+
+ //Create Telegram2:
+ given()
+ .when()
+ .body("{\"id\": \"777001-u\", \"name\" : \"Telegram2\"}")
+ .contentType("application/json")
+ .post("/api/chats")
+ .then()
+ .statusCode(201)
+ .body(emptyString());
+
+ //List all, Pineapple should be still missing now:
+ response = given()
+ .when()
+ .get("/api/chats")
+ .then()
+ .statusCode(200)
+ .extract().response();
+ assertThat(response.jsonPath().getList("name"))
+ .containsExactlyInAnyOrder("My Supergroup", "School group", "Old school group", "Telegram2");
+ }
+
+ @Test
+ public void testEntityNotFoundForDelete() {
+ given()
+ .when()
+ .delete("/api/chats/777123")
+ .then()
+ .statusCode(404)
+ .body(emptyString());
+ }
+
+ @Test
+ public void testEntityNotFoundForUpdate() {
+ given()
+ .when()
+ .body("{\"id\": \"777234-u\", \"name\" : \"Juan\"}")
+ .contentType("application/json")
+ .put("/api/chats/777234")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ public void testEntityWithoutIdForUpdate() {
+ given()
+ .when()
+ .body("{\"name\" : \"Juan\"}")
+ .contentType("application/json")
+ .put("/api/chats/777234")
+ .then()
+ .statusCode(500);
+ }
+
+ @BeforeEach
+ public void tearDown(){
+ Panache.withTransaction(() -> chatRepository.deleteById(777234L)).await().indefinitely();
+ }
+}