diff --git a/.gitignore b/.gitignore
index 0c6da4a..019b6de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -85,3 +85,4 @@ gradle-app.setting
# Others
docs/todos.md
/src/test/resources/application.properties
+/docs/sample-dataset/load_data.sh
diff --git a/README.md b/README.md
index e82d5e6..c5eb299 100644
--- a/README.md
+++ b/README.md
@@ -6,19 +6,71 @@ Instructions are [here](https://clientserveur-courses.clubinfo-clermont.fr/Notat
## About
-A "Pokemong" is a playful term for a `MongoDB` pocket monster.
+A "Pokemong" is a playful term for a MongoDB pocket monster.
-The application is developed using the Quarkus framework and uses `MongoDB` as its database.
+The application is developed using the Quarkus framework and uses MongoDB as its database.
-This application is a RESTful service designed to emulate a basic Pokemong management system. It allows users to perform
-CRUD operations on Pokemongs, trainers, moves, and types.
+This application is a RESTful service designed to emulate a basic `Pokemong` management system. It allows users to
+perform
+CRUD operations on `Pokemongs`, `Trainers`, `Moves`, and `Types`.
### 🗂️ DCM
+
### 🧬 UML Class diagram
+
+### NoSQL Schema Versioning Strategy
+
+This application uses MongoDB, a NoSQL database, which provides flexibility in our data model. While this flexibility
+has
+its advantages, it poses a unique challenge when we need to update our data model, specifically when we want to
+introduce breaking changes in the existing schema.
+
+We have adopted a schema versioning strategy to overcome this challenge and manage these changes efficiently.
+
+#### Schema Versioning Pattern
+
+Schema versioning is a pattern that involves tagging each document in a collection with a version number. This version
+number corresponds to the schema of the document and is used to handle schema changes in the code that reads these
+documents.
+
+Each entity in our model extends a `GenericVersionedEntity` class, which includes a `schemaVersion` field. This field is
+an integer that starts at 1 and is to be incremented by one with each schema change. Every change to the schema needs to
+involve the schema version number being incremented.
+
+#### Incremental Document Migration
+
+When a document is read from the database, the version number in the document is checked. If the version number is less
+than the current version, the document is updated to the current version, and the updated document is written back to
+the database. This process effectively migrates the document to the current version.
+
+In the example of the `Move` class, the codec's `decodeV1` method handles documents with a `schemaVersion` of less
+than `2`. When it reads a document with this version, it updates the `schemaVersion` to `2`, and writes the updated
+document back to the database.
+
+```java
+Move decodeV1(Document document){
+ // ...
+ // Increment the schemaVersion to the current version
+ move.setSchemaVersion(2);
+
+ // Save the updated Move object back to the database
+ moveRepository.persistOrUpdate(move);
+ // ...
+ }
+```
+
+This strategy allows for graceful schema evolution in a NoSQL environment. Instead of requiring all documents to be
+migrated at once, which can be a time-consuming operation for large collections, it enables incremental document
+migration. This approach also helps to avoid downtime during schema migration, as the application continues to function
+correctly regardless of the document version. As documents are read, they are updated to the current schema version,
+allowing the schema migration to happen gradually over time.
+
+However, note that this strategy increases write operations to the database, which could affect application performance.
+
## Prep steps
### ♨️ Java version
@@ -79,19 +131,30 @@ You can run the application in dev mode using:
## API testing
+### 🧪 Sample dataset
+
+You can find a sample dataset at `docs/sample-dataset/`. Each JSON file contains a collection.
+
+To load the `moves` collection into an existing MongoDB cluster, you may use [MongoDB Shell ("mongosh")](https://www.mongodb.com/docs/mongodb-shell/) to run
+```shell script
+mongoimport --uri=mongodb+srv://:@..mongodb.net/ --collection=moves --file=./docs/sample-dataset/moves.json
+```
+
### 🏴☠️ SwaggerUI
Thanks to this project's OpenAPI specs, you can explore the API in a lot of ways.
A popular choice is SwaggerUI -- after you run the app, just go to http://localhost:8080/q/swagger-ui and have fun.
⚠️ Unfortunately, Swagger or Quarkus or SmallRye adds the field `id` to all request examples, but in fact ***you should
-not include id** when you POST a new document.*
+NOT include id** when you POST or UPDATE a new document.* The app takes care of it for you. Same thing for the field `species` with `Pokemong` documents.
### 🩺 API testing tools
You can use an API testing tool such as [Postman](https://www.postman.com/)
or [Insomnia](https://insomnia.rest/) to test this app.
+If you use Postman, you can even import `docs/postman_collection.json`, designed to work with the `🧪 Sample dataset`.
+
### 📱 Front end (later)
Moving forward, the front end part of this app -- a different project -- might also come into play for trying out this
diff --git a/docs/postman_collection.json b/docs/postman_collection.json
new file mode 100644
index 0000000..292f8cd
--- /dev/null
+++ b/docs/postman_collection.json
@@ -0,0 +1,384 @@
+{
+ "info": {
+ "_postman_id": "11aa7a76-83a4-4b92-940f-528e29b66df8",
+ "name": "PoKeMoNg",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "25802734"
+ },
+ "item": [
+ {
+ "name": "move",
+ "item": [
+ {
+ "name": "Create 1 move",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"name\": \"Bubble beam\",\r\n \"category\": \"PHYSICAL\",\r\n \"power\": 10,\r\n \"accuracy\": 85,\r\n \"type\": \r\n {\r\n \"name\": \"WATER\",\r\n \"weakAgainst\": [\"GRASS\"],\r\n \"effectiveAgainst\": [\"FIRE\", \"GROUND\"]\r\n },\r\n \"schemaVersion\": 2\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/move",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all moves",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/move",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get 1 move",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move",
+ "60a64f7eae945a6e60b0e917"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update 1 move",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"schemaVersion\": 2,\r\n \"name\": \"Ember UPDATED\",\r\n \"category\": \"SPECIAL\",\r\n \"power\": 40,\r\n \"accuracy\": 100,\r\n \"type\": {\r\n \"name\": \"FIRE\",\r\n \"weakAgainst\": [\r\n \"WATER\",\r\n \"GROUND\"\r\n ],\r\n \"effectiveAgainst\": [\r\n \"GRASS\"\r\n ]\r\n }\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move",
+ "60a64f7eae945a6e60b0e917"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete 1 move",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e913",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "move",
+ "60a64f7eae945a6e60b0e913"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "pokemong",
+ "item": [
+ {
+ "name": "Create 1 pkmn",
+ "protocolProfileBehavior": {
+ "disabledSystemHeaders": {
+ "content-type": true
+ }
+ },
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"nickname\": \"Blappity-bloop\",\r\n \"dob\": \"2023-05-07\",\r\n \"level\": 1,\r\n \"pokedexId\": 172,\r\n \"evoStage\": 1,\r\n \"evoTrack\": [\"PICHU\", \"PIKACHU\", \"RAICHU\"],\r\n \"types\": [\r\n {\r\n \"name\": \"ELECTRIC\",\r\n \"weakAgainst\": [\"GROUND\", \"ROCK\"],\r\n \"effectiveAgainst\": [\"WATER\", \"FLYING\"]\r\n }\r\n ],\r\n \"moveSet\": \r\n [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e917\",\r\n \"name\": \"Ember\"\r\n }\r\n ],\r\n \"schemaVersion\": 1\r\n}"
+ },
+ "url": {
+ "raw": "http://localhost:8080/pokemong",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all pkmn",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get 1 pkmn",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "60a64f7eae945a6e60b0e911"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update 1 pkmn",
+ "request": {
+ "method": "PUT",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"schemaVersion\": 1,\n \"nickname\": \"Sparky UPDATED\",\n \"dob\": \"1994-02-18\",\n \"level\": 15,\n \"pokedexId\": 1,\n \"evoStage\": 1,\n \"evoTrack\": [\n \"BULBASAUR\",\n \"IVYSAUR\",\n \"VENUSAUR\"\n ],\n \"trainer\": \"60a64f7eae945a6e60b0e914\",\n \"types\": [\n {\n \"name\": \"GRASS\",\n \"weakAgainst\": [\n \"FIRE\"\n ],\n \"effectiveAgainst\": [\n \"WATER\",\n \"GROUND\"\n ]\n }\n ],\n \"moveSet\": [\n {\n \"id\": \"60a64f7eae945a6e60b0e912\",\n \"name\": \"Vine Whip\"\n }\n ]\n}"
+ },
+ "url": {
+ "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "60a64f7eae945a6e60b0e911"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete 1 pkmn",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e916",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "pokemong",
+ "60a64f7eae945a6e60b0e916"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "trainer",
+ "item": [
+ {
+ "name": "Create 1 trainer",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Bloop\",\r\n \"dob\": \"1997-02-18\",\r\n \"wins\": 1,\r\n \"losses\": 50,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e915\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/trainer",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all trainers",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/trainer",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get 1 trainer",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer",
+ "60a64f7eae945a6e60b0e914"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update 1 trainer",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Brock\",\r\n \"dob\": \"1994-02-18\",\r\n \"wins\": 60,\r\n \"losses\": 60,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e914\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e915",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer",
+ "60a64f7eae945a6e60b0e915"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete 1 trainer",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "trainer",
+ "60a64f7eae945a6e60b0e914"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/sample-dataset/moves.json b/docs/sample-dataset/moves.json
new file mode 100644
index 0000000..0bfac4f
--- /dev/null
+++ b/docs/sample-dataset/moves.json
@@ -0,0 +1,40 @@
+{
+ "_id": { "$oid":"60a64f7eae945a6e60b0e912" },
+ "schemaVersion": 1,
+ "name": "Vine Whip",
+ "power": 45,
+ "pp": 15,
+ "category": "PHYSICAL",
+ "accuracy": 100,
+ "type": {
+ "name": "GRASS",
+ "weakAgainst": ["FIRE"],
+ "effectiveAgainst": ["WATER", "GROUND"]
+ }
+}
+{
+ "_id": { "$oid":"60a64f7eae945a6e60b0e913" },
+ "schemaVersion": 2,
+ "name": "Tackle",
+ "power": 40,
+ "category": "PHYSICAL",
+ "accuracy": 100,
+ "type": {
+ "name": "NORMAL",
+ "weakAgainst": ["ROCK"],
+ "effectiveAgainst": []
+ }
+}
+{
+ "_id": { "$oid":"60a64f7eae945a6e60b0e917" },
+ "schemaVersion": 2,
+ "name": "Ember",
+ "power": 40,
+ "category": "SPECIAL",
+ "accuracy": 100,
+ "type": {
+ "name": "FIRE",
+ "weakAgainst": ["WATER", "GROUND"],
+ "effectiveAgainst": ["GRASS"]
+ }
+}
diff --git a/docs/sample-dataset/pokemongs.json b/docs/sample-dataset/pokemongs.json
new file mode 100644
index 0000000..7ba88b9
--- /dev/null
+++ b/docs/sample-dataset/pokemongs.json
@@ -0,0 +1,98 @@
+{
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e911"
+ },
+ "schemaVersion": 1,
+ "nickname": "Sparky",
+ "dob": {
+ "$date": {
+ "$numberLong": "761597551000"
+ }
+ },
+ "level": 15,
+ "pokedexId": 1,
+ "evoStage": 1,
+ "evoTrack": [
+ "BULBASAUR",
+ "IVYSAUR",
+ "VENUSAUR"
+ ],
+ "trainer": {
+ "$oid": "60a64f7eae945a6e60b0e914"
+ },
+ "types": [
+ {
+ "name": "GRASS",
+ "weakAgainst": [
+ "FIRE"
+ ],
+ "effectiveAgainst": [
+ "WATER",
+ "GROUND"
+ ]
+ }
+ ],
+ "moveSet": [
+ {
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e913"
+ },
+ "name": "Tackle"
+ },
+ {
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e912"
+ },
+ "name": "Vine Whip"
+ }
+ ]
+}
+{
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e916"
+ },
+ "schemaVersion": 1,
+ "nickname": "Blazey",
+ "dob": {
+ "$date": {
+ "$numberLong": "761597651000"
+ }
+ },
+ "level": 10,
+ "pokedexId": 4,
+ "evoStage": 0,
+ "evoTrack": [
+ "CHARMANDER",
+ "CHARMELEON",
+ "CHARIZARD"
+ ],
+ "trainer": {
+ "$oid": "60a64f7eae945a6e60b0e915"
+ },
+ "types": [
+ {
+ "name": "FIRE",
+ "weakAgainst": [
+ "WATER",
+ "GROUND"
+ ],
+ "effectiveAgainst": [
+ "GRASS"
+ ]
+ }
+ ],
+ "moveSet": [
+ {
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e913"
+ },
+ "name": "Tackle"
+ },
+ {
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e917"
+ },
+ "name": "Ember"
+ }
+ ]
+}
diff --git a/docs/sample-dataset/trainers.json b/docs/sample-dataset/trainers.json
new file mode 100644
index 0000000..913b61b
--- /dev/null
+++ b/docs/sample-dataset/trainers.json
@@ -0,0 +1,56 @@
+{
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e914"
+ },
+ "schemaVersion": 1,
+ "name": "Ash",
+ "dob": {
+ "$date": {
+ "$numberLong": "761598551000"
+ }
+ },
+ "wins": 100,
+ "losses": 50,
+ "pastOpponents": [
+ {
+ "$oid": "60a64f7eae945a6e60b0e915"
+ }
+ ],
+ "pokemongs": [
+ {
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e911"
+ },
+ "nickname": "Sparky",
+ "species": "IVYSAUR"
+ }
+ ]
+}
+{
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e915"
+ },
+ "schemaVersion": 1,
+ "name": "Brock",
+ "dob": {
+ "$date": {
+ "$numberLong": "761596551000"
+ }
+ },
+ "wins": 70,
+ "losses": 60,
+ "pastOpponents": [
+ {
+ "$oid": "60a64f7eae945a6e60b0e914"
+ }
+ ],
+ "pokemongs": [
+ {
+ "_id": {
+ "$oid": "60a64f7eae945a6e60b0e916"
+ },
+ "nickname": "Blazey",
+ "species": "CHARMANDER"
+ }
+ ]
+}
diff --git a/src/main/java/fr/uca/iut/codecs/GenericCodec.java b/src/main/java/fr/uca/iut/codecs/GenericCodec.java
index 25faad3..ed21526 100644
--- a/src/main/java/fr/uca/iut/codecs/GenericCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/GenericCodec.java
@@ -9,6 +9,7 @@ import org.bson.codecs.CollectibleCodec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId;
+import org.jetbrains.annotations.NotNull;
public abstract class GenericCodec implements CollectibleCodec {
@@ -27,12 +28,12 @@ public abstract class GenericCodec implements Collectib
}
@Override
- public boolean documentHasId(T document) {
+ public boolean documentHasId(@NotNull T document) {
return document.getId() != null;
}
@Override
- public BsonValue getDocumentId(T document) {
+ public BsonValue getDocumentId(@NotNull T document) {
return new BsonObjectId(new ObjectId(document.getId()));
}
diff --git a/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java b/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java
index 13b7ec8..deb2d6c 100644
--- a/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java
@@ -1,78 +1,122 @@
-package fr.uca.iut.codecs.move;
-
-import com.mongodb.MongoClientSettings;
-import fr.uca.iut.codecs.GenericCodec;
-import fr.uca.iut.codecs.type.TypeCodecUtil;
-import fr.uca.iut.entities.Move;
-import fr.uca.iut.entities.Type;
-import fr.uca.iut.utils.enums.MoveCategoryName;
-import org.bson.BsonReader;
-import org.bson.BsonWriter;
-import org.bson.Document;
-import org.bson.codecs.Codec;
-import org.bson.codecs.DecoderContext;
-import org.bson.codecs.EncoderContext;
-import org.bson.types.ObjectId;
-
-public class MoveCodec extends GenericCodec {
- private final Codec documentCodec;
-
- public MoveCodec() {
- this.documentCodec = MongoClientSettings.getDefaultCodecRegistry()
- .get(Document.class);
- }
-
- @Override
- public void encode(BsonWriter writer, Move move, EncoderContext encoderContext) {
- Document doc = new Document();
-
- doc.put("_id", new ObjectId(move.getId()));
-
- doc.put("name", move.getName());
-
- doc.put("category", move.getCategory());
-
- doc.put("power", move.getPower());
-
- doc.put("accuracy", move.getAccuracy());
-
- Type moveType = move.getType();
- Document typeDoc = new Document();
- typeDoc.put("name",
- moveType.getName()
- .toString());
- typeDoc.put("weakAgainst", moveType.getWeakAgainst());
- typeDoc.put("effectiveAgainst", moveType.getEffectiveAgainst());
- doc.put("type", typeDoc);
-
- documentCodec.encode(writer, doc, encoderContext);
- }
-
- @Override
- public Class getEncoderClass() {
- return Move.class;
- }
-
- @Override
- public Move decode(BsonReader reader, DecoderContext decoderContext) {
- Document document = documentCodec.decode(reader, decoderContext);
- Move move = new Move();
-
- move.setId(document.getObjectId("_id")
- .toString());
-
- move.setName(document.getString("name"));
-
- move.setCategory(MoveCategoryName.valueOf(document.getString("category")));
-
- move.setPower(document.getInteger("power"));
-
- move.setAccuracy(document.getInteger("accuracy"));
-
- Document typeDoc = (Document) document.get("type");
-
- move.setType(TypeCodecUtil.extractType(typeDoc));
-
- return move;
- }
-}
+package fr.uca.iut.codecs.move;
+
+import com.mongodb.MongoClientSettings;
+import fr.uca.iut.codecs.GenericCodec;
+import fr.uca.iut.codecs.type.TypeCodecUtil;
+import fr.uca.iut.entities.Move;
+import fr.uca.iut.entities.embedded.Type;
+import fr.uca.iut.utils.enums.MoveCategoryName;
+import org.bson.BsonReader;
+import org.bson.BsonWriter;
+import org.bson.Document;
+import org.bson.codecs.Codec;
+import org.bson.codecs.DecoderContext;
+import org.bson.codecs.EncoderContext;
+import org.bson.types.ObjectId;
+import org.jetbrains.annotations.NotNull;
+
+public class MoveCodec extends GenericCodec {
+ private final Codec documentCodec;
+
+ public MoveCodec() {
+ this.documentCodec = MongoClientSettings.getDefaultCodecRegistry()
+ .get(Document.class);
+ }
+
+ @Override
+ public void encode(BsonWriter writer, @NotNull Move move, EncoderContext encoderContext) {
+ Document doc = new Document();
+
+ doc.put("_id", new ObjectId(move.getId()));
+
+ doc.put("schemaVersion", move.getSchemaVersion());
+
+ doc.put("name", move.getName());
+
+ doc.put("category", move.getCategory());
+
+ doc.put("power", move.getPower());
+
+ doc.put("accuracy", move.getAccuracy());
+
+ Type moveType = move.getType();
+ Document typeDoc = new Document();
+ typeDoc.put("name",
+ moveType.getName()
+ .toString());
+ typeDoc.put("weakAgainst", moveType.getWeakAgainst());
+ typeDoc.put("effectiveAgainst", moveType.getEffectiveAgainst());
+ doc.put("type", typeDoc);
+
+ documentCodec.encode(writer, doc, encoderContext);
+ }
+
+ @Override
+ public Class getEncoderClass() {
+ return Move.class;
+ }
+
+ @Override
+ public Move decode(BsonReader reader, DecoderContext decoderContext) {
+
+ Document document = documentCodec.decode(reader, decoderContext);
+
+ Integer schemaVersion = document.getInteger("schemaVersion");
+
+ return switch (schemaVersion) {
+ case 1 -> decodeV1(document);
+ case 2 -> decodeV2(document);
+ default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
+ };
+ }
+
+ private @NotNull Move decodeV1(@NotNull Document document) {
+ Move move = new Move();
+
+ move.setId(document.getObjectId("_id")
+ .toString());
+
+ move.setSchemaVersion(document.getInteger("schemaVersion"));
+
+ move.setName(document.getString("name"));
+
+ move.setCategory(MoveCategoryName.valueOf(document.getString("category")));
+
+ move.setPower(document.getInteger("power"));
+
+ move.setAccuracy(document.getInteger("accuracy"));
+
+ Document typeDoc = (Document) document.get("type");
+
+ move.setType(TypeCodecUtil.extractType(typeDoc));
+
+ // Read and discard the old pp field
+ Integer pp = document.getInteger("pp");
+
+ return move;
+ }
+
+ private @NotNull Move decodeV2(@NotNull Document document) {
+ Move move = new Move();
+
+ move.setId(document.getObjectId("_id")
+ .toString());
+
+ move.setSchemaVersion(document.getInteger("schemaVersion"));
+
+ move.setName(document.getString("name"));
+
+ move.setCategory(MoveCategoryName.valueOf(document.getString("category")));
+
+ move.setPower(document.getInteger("power"));
+
+ move.setAccuracy(document.getInteger("accuracy"));
+
+ Document typeDoc = (Document) document.get("type");
+
+ move.setType(TypeCodecUtil.extractType(typeDoc));
+
+ return move;
+ }
+
+}
diff --git a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java
index 4d65c23..f734503 100644
--- a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java
@@ -4,8 +4,8 @@ import com.mongodb.MongoClientSettings;
import fr.uca.iut.codecs.GenericCodec;
import fr.uca.iut.codecs.type.TypeCodecUtil;
import fr.uca.iut.entities.Pokemong;
-import fr.uca.iut.entities.PokemongMove;
-import fr.uca.iut.entities.Type;
+import fr.uca.iut.entities.denormalized.PokemongMove;
+import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.utils.enums.PokemongName;
import org.bson.BsonReader;
import org.bson.BsonWriter;
@@ -14,6 +14,7 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId;
+import org.jetbrains.annotations.NotNull;
import java.time.ZoneId;
import java.util.Date;
@@ -26,21 +27,23 @@ public class PokemongCodec extends GenericCodec {
public PokemongCodec() {
this.documentCodec = MongoClientSettings.getDefaultCodecRegistry()
- .get(Document.class);
+ .get(Document.class);
}
@Override
- public void encode(BsonWriter writer, Pokemong pokemong, EncoderContext encoderContext) {
+ public void encode(BsonWriter writer, @NotNull Pokemong pokemong, EncoderContext encoderContext) {
Document doc = new Document();
doc.put("_id", new ObjectId(pokemong.getId()));
+ doc.put("schemaVersion", pokemong.getSchemaVersion());
+
doc.put("nickname", pokemong.getNickname());
doc.put("dob",
Date.from(pokemong.getDob()
- .atStartOfDay(ZoneId.systemDefault())
- .toInstant()));
+ .atStartOfDay(ZoneId.systemDefault())
+ .toInstant()));
doc.put("level", pokemong.getLevel());
@@ -49,9 +52,9 @@ public class PokemongCodec extends GenericCodec {
doc.put("evoStage", pokemong.getEvoStage());
List evoTrack = pokemong.getEvoTrack()
- .stream()
- .map(Enum::name)
- .collect(Collectors.toList());
+ .stream()
+ .map(Enum::name)
+ .collect(Collectors.toList());
doc.put("evoTrack", evoTrack);
if (pokemong.getTrainer() != null) {
@@ -59,36 +62,36 @@ public class PokemongCodec extends GenericCodec {
}
List types = pokemong.getTypes()
- .stream()
- .map(type -> {
- Document typeDoc = new Document();
- typeDoc.put("name",
- type.getName()
- .name());
- List weakAgainst = type.getWeakAgainst()
- .stream()
- .map(Enum::name)
- .collect(Collectors.toList());
- typeDoc.put("weakAgainst", weakAgainst);
- List effectiveAgainst = type.getEffectiveAgainst()
- .stream()
- .map(Enum::name)
- .collect(Collectors.toList());
- typeDoc.put("effectiveAgainst", effectiveAgainst);
- return typeDoc;
- })
- .collect(Collectors.toList());
+ .stream()
+ .map(type -> {
+ Document typeDoc = new Document();
+ typeDoc.put("name",
+ type.getName()
+ .name());
+ List weakAgainst = type.getWeakAgainst()
+ .stream()
+ .map(Enum::name)
+ .collect(Collectors.toList());
+ typeDoc.put("weakAgainst", weakAgainst);
+ List effectiveAgainst = type.getEffectiveAgainst()
+ .stream()
+ .map(Enum::name)
+ .collect(Collectors.toList());
+ typeDoc.put("effectiveAgainst", effectiveAgainst);
+ return typeDoc;
+ })
+ .collect(Collectors.toList());
doc.put("types", types);
List moveSetDocs = pokemong.getMoveSet()
- .stream()
- .map(move -> {
- Document moveDoc = new Document();
- moveDoc.put("_id", new ObjectId(move.getId()));
- moveDoc.put("name", move.getName());
- return moveDoc;
- })
- .collect(Collectors.toList());
+ .stream()
+ .map(move -> {
+ Document moveDoc = new Document();
+ moveDoc.put("_id", new ObjectId(move.getId()));
+ moveDoc.put("name", move.getName());
+ return moveDoc;
+ })
+ .collect(Collectors.toList());
doc.put("moveSet", moveSetDocs);
documentCodec.encode(writer, doc, encoderContext);
@@ -102,18 +105,30 @@ public class PokemongCodec extends GenericCodec {
@Override
public Pokemong decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, decoderContext);
+
+ Integer schemaVersion = document.getInteger("schemaVersion");
+
+ return switch (schemaVersion) {
+ case 1 -> decodeV1(document);
+ default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
+ };
+ }
+
+ private @NotNull Pokemong decodeV1(@NotNull Document document) {
Pokemong pokemong = new Pokemong();
pokemong.setId(document.getObjectId("_id")
- .toString());
+ .toString());
+
+ pokemong.setSchemaVersion(document.getInteger("schemaVersion"));
pokemong.setNickname(document.getString("nickname"));
Date dob = document.getDate("dob");
if (dob != null) {
pokemong.setDob(dob.toInstant()
- .atZone(ZoneId.systemDefault())
- .toLocalDate());
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate());
}
pokemong.setLevel(document.getInteger("level"));
@@ -123,9 +138,9 @@ public class PokemongCodec extends GenericCodec {
pokemong.setEvoStage(document.getInteger("evoStage"));
List evoTrack = document.getList("evoTrack", String.class)
- .stream()
- .map(PokemongName::valueOf)
- .collect(Collectors.toList());
+ .stream()
+ .map(PokemongName::valueOf)
+ .collect(Collectors.toList());
pokemong.setEvoTrack(evoTrack);
ObjectId trainerId = document.getObjectId("trainer");
@@ -133,21 +148,21 @@ public class PokemongCodec extends GenericCodec {
pokemong.setTrainer(trainerId.toString());
}
- List types = document.getList("types", Document.class)
- .stream()
- .map(TypeCodecUtil::extractType)
- .collect(Collectors.toList());
+ Set types = document.getList("types", Document.class)
+ .stream()
+ .map(TypeCodecUtil::extractType)
+ .collect(Collectors.toSet());
pokemong.setTypes(types);
Set moveSet = document.getList("moveSet", Document.class)
- .stream()
- .map(pokemongMoveDoc -> {
- PokemongMove move = new PokemongMove();
- move.setId(((ObjectId) pokemongMoveDoc.get("_id")).toString());
- move.setName(pokemongMoveDoc.getString("name"));
- return move;
- })
- .collect(Collectors.toSet());
+ .stream()
+ .map(pokemongMoveDoc -> {
+ PokemongMove move = new PokemongMove();
+ move.setId(((ObjectId) pokemongMoveDoc.get("_id")).toString());
+ move.setName(pokemongMoveDoc.getString("name"));
+ return move;
+ })
+ .collect(Collectors.toSet());
pokemong.setMoveSet(moveSet);
return pokemong;
diff --git a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java
index 7327c19..fb1db0d 100644
--- a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java
+++ b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java
@@ -5,11 +5,12 @@ import fr.uca.iut.entities.Pokemong;
import org.bson.codecs.Codec;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
+import org.jetbrains.annotations.NotNull;
public class PokemongCodecProvider implements CodecProvider {
@Nullable
@Override
- public Codec get(Class clazz, CodecRegistry registry) {
+ public Codec get(@NotNull Class clazz, CodecRegistry registry) {
if (clazz.equals(Pokemong.class)) {
return (Codec) new PokemongCodec();
}
diff --git a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java
index f9c0041..ccd874b 100644
--- a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java
@@ -3,7 +3,7 @@ package fr.uca.iut.codecs.trainer;
import com.mongodb.MongoClientSettings;
import fr.uca.iut.codecs.GenericCodec;
import fr.uca.iut.entities.Trainer;
-import fr.uca.iut.entities.TrainerPokemong;
+import fr.uca.iut.entities.denormalized.TrainerPokemong;
import fr.uca.iut.utils.enums.PokemongName;
import org.bson.BsonReader;
import org.bson.BsonWriter;
@@ -12,11 +12,13 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId;
+import org.jetbrains.annotations.NotNull;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
public class TrainerCodec extends GenericCodec {
@@ -24,21 +26,66 @@ public class TrainerCodec extends GenericCodec {
public TrainerCodec() {
this.documentCodec = MongoClientSettings.getDefaultCodecRegistry()
- .get(Document.class);
+ .get(Document.class);
+ }
+
+ @NotNull
+ private static Trainer decodeV1(Document document) {
+ Trainer trainer = new Trainer();
+
+ trainer.setId(document.getObjectId("_id")
+ .toString());
+
+ trainer.setSchemaVersion(document.getInteger("schemaVersion"));
+
+ trainer.setName(document.getString("name"));
+
+ Date dob = document.getDate("dob");
+ if (dob != null) {
+ trainer.setDob(dob.toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate());
+ }
+
+ trainer.setWins(document.getInteger("wins"));
+
+ trainer.setLosses(document.getInteger("losses"));
+
+ List pastOpponentsIds = document.getList("pastOpponents", ObjectId.class)
+ .stream()
+ .map(ObjectId::toString)
+ .collect(Collectors.toList());
+ trainer.setPastOpponents(pastOpponentsIds);
+
+ Set pokemongList = document
+ .getList("pokemongs", Document.class)
+ .stream()
+ .map(pokemongDoc -> {
+ TrainerPokemong pokemong = new TrainerPokemong();
+ pokemong.setId(((ObjectId) pokemongDoc.get("_id")).toString());
+ pokemong.setNickname(pokemongDoc.getString("nickname"));
+ pokemong.setSpecies(PokemongName.valueOf(pokemongDoc.getString("species")));
+ return pokemong;
+ })
+ .collect(Collectors.toSet());
+ trainer.setPokemongs(pokemongList);
+ return trainer;
}
@Override
- public void encode(BsonWriter writer, Trainer trainer, EncoderContext encoderContext) {
+ public void encode(BsonWriter writer, @NotNull Trainer trainer, EncoderContext encoderContext) {
Document doc = new Document();
doc.put("_id", new ObjectId(trainer.getId()));
+ doc.put("schemaVersion", trainer.getSchemaVersion());
+
doc.put("name", trainer.getName());
LocalDate dob = trainer.getDob();
if (dob != null) {
doc.put("dob", Date.from(dob.atStartOfDay(ZoneId.systemDefault())
- .toInstant()));
+ .toInstant()));
}
doc.put("wins", trainer.getWins());
@@ -46,23 +93,23 @@ public class TrainerCodec extends GenericCodec {
doc.put("losses", trainer.getLosses());
List pastOpponentsIds = trainer.getPastOpponents()
- .stream()
- .map(ObjectId::new)
- .collect(Collectors.toList());
+ .stream()
+ .map(ObjectId::new)
+ .collect(Collectors.toList());
doc.put("pastOpponents", pastOpponentsIds);
List pokemongListDoc = trainer.getPokemongs()
- .stream()
- .map(pokemong -> {
- Document moveDoc = new Document();
- moveDoc.put("_id", new ObjectId(pokemong.getId()));
- moveDoc.put("nickname", pokemong.getNickname());
- moveDoc.put("species",
- pokemong.getSpecies()
- .name());
- return moveDoc;
- })
- .collect(Collectors.toList());
+ .stream()
+ .map(pokemong -> {
+ Document moveDoc = new Document();
+ moveDoc.put("_id", new ObjectId(pokemong.getId()));
+ moveDoc.put("nickname", pokemong.getNickname());
+ moveDoc.put("species",
+ pokemong.getSpecies()
+ .name());
+ return moveDoc;
+ })
+ .collect(Collectors.toList());
doc.put("pokemongs", pokemongListDoc);
documentCodec.encode(writer, doc, encoderContext);
@@ -76,42 +123,12 @@ public class TrainerCodec extends GenericCodec {
@Override
public Trainer decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, decoderContext);
- Trainer trainer = new Trainer();
-
- trainer.setId(document.getObjectId("_id")
- .toString());
-
- trainer.setName(document.getString("name"));
-
- Date dob = document.getDate("dob");
- if (dob != null) {
- trainer.setDob(dob.toInstant()
- .atZone(ZoneId.systemDefault())
- .toLocalDate());
- }
-
- trainer.setWins(document.getInteger("wins"));
-
- trainer.setLosses(document.getInteger("losses"));
- List pastOpponentsIds = document.getList("pastOpponents", ObjectId.class)
- .stream()
- .map(ObjectId::toString)
- .collect(Collectors.toList());
- trainer.setPastOpponents(pastOpponentsIds);
+ Integer schemaVersion = document.getInteger("schemaVersion");
- List pokemongList = document
- .getList("pokemongs", Document.class)
- .stream()
- .map(pokemongDoc -> {
- TrainerPokemong pokemong = new TrainerPokemong();
- pokemong.setId(((ObjectId) pokemongDoc.get("_id")).toString());
- pokemong.setNickname(pokemongDoc.getString("nickname"));
- pokemong.setSpecies(PokemongName.valueOf(pokemongDoc.getString("species")));
- return pokemong;
- })
- .collect(Collectors.toList());
- trainer.setPokemongs(pokemongList);
- return trainer;
+ return switch (schemaVersion) {
+ case 1 -> decodeV1(document);
+ default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
+ };
}
}
diff --git a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java
index 0c3e37a..87aac26 100644
--- a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java
+++ b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java
@@ -5,11 +5,12 @@ import fr.uca.iut.entities.Trainer;
import org.bson.codecs.Codec;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
+import org.jetbrains.annotations.NotNull;
public class TrainerCodecProvider implements CodecProvider {
@Nullable
@Override
- public Codec get(Class clazz, CodecRegistry registry) {
+ public Codec get(@NotNull Class clazz, CodecRegistry registry) {
if (clazz.equals(Trainer.class)) {
return (Codec) new TrainerCodec();
}
diff --git a/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java b/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java
index bd45965..a2d53b6 100644
--- a/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java
+++ b/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java
@@ -1,6 +1,6 @@
package fr.uca.iut.codecs.type;
-import fr.uca.iut.entities.Type;
+import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.utils.enums.TypeName;
import org.bson.Document;
@@ -12,15 +12,15 @@ public class TypeCodecUtil {
Type type = new Type();
type.setName(TypeName.valueOf(typeDoc.getString("name")));
List weakAgainst = typeDoc.getList("weakAgainst", String.class)
- .stream()
- .map(TypeName::valueOf)
- .collect(Collectors.toList());
+ .stream()
+ .map(TypeName::valueOf)
+ .collect(Collectors.toList());
type.setWeakAgainst(weakAgainst);
List effectiveAgainst = typeDoc.getList("effectiveAgainst",
- String.class)
- .stream()
- .map(TypeName::valueOf)
- .collect(Collectors.toList());
+ String.class)
+ .stream()
+ .map(TypeName::valueOf)
+ .collect(Collectors.toList());
type.setEffectiveAgainst(effectiveAgainst);
return type;
}
diff --git a/src/main/java/fr/uca/iut/controllers/GenericController.java b/src/main/java/fr/uca/iut/controllers/GenericController.java
index 70b30d2..6c11aeb 100644
--- a/src/main/java/fr/uca/iut/controllers/GenericController.java
+++ b/src/main/java/fr/uca/iut/controllers/GenericController.java
@@ -22,24 +22,23 @@ public abstract class GenericController {
T entity = service.getOneById(id);
if (entity != null) {
return Response.ok(entity)
- .build();
- }
- else {
+ .build();
+ } else {
return Response.status(Response.Status.NOT_FOUND)
- .entity("Entity not found for id: " + id)
- .build();
+ .entity("Entity not found for id: " + id)
+ .build();
}
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
- .entity("Invalid id format: " + id)
- .build();
+ .entity(e.getMessage())
+ .build();
}
}
@GET
public Response getAll() {
return Response.ok(service.getAll())
- .build();
+ .build();
}
@POST
@@ -47,17 +46,16 @@ public abstract class GenericController {
public Response createOne(T entity) {
try {
- service.validateOne(entity);
T newEntity = service.addOne(entity);
return Response.status(Response.Status.CREATED)
- .entity(newEntity)
- .build();
+ .entity(newEntity)
+ .build();
} catch (NonValidEntityException e) {
return Response.status(Response.Status.BAD_REQUEST)
- .entity(e.getMessage())
- .build();
+ .entity(e.getMessage())
+ .build();
}
}
@@ -66,28 +64,22 @@ public abstract class GenericController {
@Consumes(MediaType.APPLICATION_JSON)
public Response updateOne(@PathParam("id") String id, T entity) {
try {
- service.validateOne(entity);
entity.setId(id);
T updatedEntity = service.updateOne(entity);
if (updatedEntity != null) {
return Response.status(Response.Status.OK)
- .entity(updatedEntity)
- .build();
- }
- else {
+ .entity(updatedEntity)
+ .build();
+ } else {
return Response.status(Response.Status.NOT_FOUND)
- .entity("Entity not found for id: " + id)
- .build();
+ .entity("Entity not found for id: " + id)
+ .build();
}
- } catch (IllegalArgumentException e) {
- return Response.status(Response.Status.BAD_REQUEST)
- .entity("Invalid id format: " + id)
- .build();
- } catch (NonValidEntityException e) {
+ } catch (IllegalArgumentException | NonValidEntityException e) {
return Response.status(Response.Status.BAD_REQUEST)
- .entity(e.getMessage())
- .build();
+ .entity(e.getMessage())
+ .build();
}
}
@@ -97,12 +89,12 @@ public abstract class GenericController {
try {
service.deleteOneById(id);
return Response.ok()
- .build();
+ .build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
- .entity("Invalid id format: " + id)
- .build();
+ .entity(e.getMessage())
+ .build();
}
}
}
diff --git a/src/main/java/fr/uca/iut/entities/GenericVersionedEntity.java b/src/main/java/fr/uca/iut/entities/GenericVersionedEntity.java
new file mode 100644
index 0000000..7ede19a
--- /dev/null
+++ b/src/main/java/fr/uca/iut/entities/GenericVersionedEntity.java
@@ -0,0 +1,21 @@
+package fr.uca.iut.entities;
+
+/**
+ * The strategy for incrementing the schema version number is simple.
+ *
+ * `schemaVersion` will have to start at 1, and need to be incremented by one at each schema change.
+ *
+ * Every change to the schema needs to involve the schema version number being incremented.
+ */
+public abstract class GenericVersionedEntity extends GenericEntity {
+
+ private Integer schemaVersion;
+
+ public Integer getSchemaVersion() {
+ return schemaVersion;
+ }
+
+ public void setSchemaVersion(Integer schemaVersion) {
+ this.schemaVersion = schemaVersion;
+ }
+}
diff --git a/src/main/java/fr/uca/iut/entities/Move.java b/src/main/java/fr/uca/iut/entities/Move.java
index da98c37..2aa1242 100644
--- a/src/main/java/fr/uca/iut/entities/Move.java
+++ b/src/main/java/fr/uca/iut/entities/Move.java
@@ -1,55 +1,58 @@
-package fr.uca.iut.entities;
-
-import fr.uca.iut.utils.enums.MoveCategoryName;
-
-public class Move extends GenericEntity {
- public static final String COLLECTION_NAME = "moves";
-
- private String name;
- private MoveCategoryName category;
- private Integer power;
- private Integer accuracy;
- private Type type;
-
- public Move() {}
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public MoveCategoryName getCategory() {
- return category;
- }
-
- public void setCategory(MoveCategoryName category) {
- this.category = category;
- }
-
- public Integer getPower() {
- return power;
- }
-
- public void setPower(Integer power) {
- this.power = power;
- }
-
- public Integer getAccuracy() {
- return accuracy;
- }
-
- public void setAccuracy(Integer accuracy) {
- this.accuracy = accuracy;
- }
-
- public Type getType() {
- return type;
- }
-
- public void setType(Type type) {
- this.type = type;
- }
+package fr.uca.iut.entities;
+
+import fr.uca.iut.entities.embedded.Type;
+import fr.uca.iut.utils.enums.MoveCategoryName;
+
+public class Move extends GenericVersionedEntity {
+ public static final String COLLECTION_NAME = "moves";
+ public static final Integer LATEST_SCHEMA_VERSION = 2;
+
+ private String name;
+ private MoveCategoryName category;
+ private Integer power;
+ private Integer accuracy;
+ private Type type;
+
+ public Move() {
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public MoveCategoryName getCategory() {
+ return category;
+ }
+
+ public void setCategory(MoveCategoryName category) {
+ this.category = category;
+ }
+
+ public Integer getPower() {
+ return power;
+ }
+
+ public void setPower(Integer power) {
+ this.power = power;
+ }
+
+ public Integer getAccuracy() {
+ return accuracy;
+ }
+
+ public void setAccuracy(Integer accuracy) {
+ this.accuracy = accuracy;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public void setType(Type type) {
+ this.type = type;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/fr/uca/iut/entities/Pokemong.java b/src/main/java/fr/uca/iut/entities/Pokemong.java
index 1acd9e4..71e0248 100644
--- a/src/main/java/fr/uca/iut/entities/Pokemong.java
+++ b/src/main/java/fr/uca/iut/entities/Pokemong.java
@@ -1,6 +1,8 @@
package fr.uca.iut.entities;
import com.mongodb.lang.Nullable;
+import fr.uca.iut.entities.denormalized.PokemongMove;
+import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.utils.enums.PokemongName;
import java.time.LocalDate;
@@ -8,9 +10,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
-public class Pokemong extends GenericEntity {
+public class Pokemong extends GenericVersionedEntity {
public static final String COLLECTION_NAME = "pokemongs";
+ public static final Integer LATEST_SCHEMA_VERSION = 1;
+
@Nullable
private String nickname;
private LocalDate dob;
@@ -20,14 +24,15 @@ public class Pokemong extends GenericEntity {
private List evoTrack;
@Nullable
private String trainer;
- private List types;
+ private Set types;
/**
* pokemong.moveSet: [{_id: ObjectId, name: String}]
*/
private Set moveSet;
- public Pokemong() {}
+ public Pokemong() {
+ }
@Nullable
public String getNickname() {
@@ -71,11 +76,11 @@ public class Pokemong extends GenericEntity {
this.trainer = trainer;
}
- public List getTypes() {
- return Collections.unmodifiableList(types);
+ public Set getTypes() {
+ return Collections.unmodifiableSet(types);
}
- public void setTypes(List types) {
+ public void setTypes(Set types) {
this.types = types;
}
@@ -96,8 +101,7 @@ public class Pokemong extends GenericEntity {
public void updateMove(String id, String name) {
for (PokemongMove move : moveSet) {
if (move.getId()
- .equals(id))
- {
+ .equals(id)) {
move.setName(name);
break;
}
@@ -109,7 +113,11 @@ public class Pokemong extends GenericEntity {
}
public List getEvoTrack() {
- return evoTrack;
+ return Collections.unmodifiableList(evoTrack);
+ }
+
+ public void setEvoTrack(List evoTrack) {
+ this.evoTrack = evoTrack;
}
public Integer getEvoStage() {
@@ -119,9 +127,5 @@ public class Pokemong extends GenericEntity {
public void setEvoStage(Integer evoStage) {
this.evoStage = evoStage;
}
-
- public void setEvoTrack(List evoTrack) {
- this.evoTrack = evoTrack;
- }
}
diff --git a/src/main/java/fr/uca/iut/entities/Trainer.java b/src/main/java/fr/uca/iut/entities/Trainer.java
index 6586349..a7aae20 100644
--- a/src/main/java/fr/uca/iut/entities/Trainer.java
+++ b/src/main/java/fr/uca/iut/entities/Trainer.java
@@ -1,20 +1,25 @@
package fr.uca.iut.entities;
+import fr.uca.iut.entities.denormalized.TrainerPokemong;
+
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
-public class Trainer extends GenericEntity {
+public class Trainer extends GenericVersionedEntity {
public static final String COLLECTION_NAME = "trainers";
+ public static final Integer LATEST_SCHEMA_VERSION = 1;
private String name;
private LocalDate dob;
private Integer wins;
private Integer losses;
private List pastOpponents;
- private List pokemongs;
+ private Set pokemongs;
- public Trainer() {}
+ public Trainer() {
+ }
public String getName() {
return name;
@@ -56,11 +61,19 @@ public class Trainer extends GenericEntity {
this.pastOpponents = pastOpponents;
}
- public List getPokemongs() {
- return Collections.unmodifiableList(pokemongs);
+ public Set getPokemongs() {
+ return Collections.unmodifiableSet(pokemongs);
}
- public void setPokemongs(List pokemongs) {
+ public void setPokemongs(Set pokemongs) {
this.pokemongs = pokemongs;
}
+
+ public void addPokemong(TrainerPokemong trainerPokemong) {
+ pokemongs.add(trainerPokemong);
+ }
+
+ public void removePokemong(String id) {
+ pokemongs.removeIf(trainerPokemong -> trainerPokemong.getId().equals(id));
+ }
}
diff --git a/src/main/java/fr/uca/iut/entities/PokemongMove.java b/src/main/java/fr/uca/iut/entities/denormalized/PokemongMove.java
similarity index 63%
rename from src/main/java/fr/uca/iut/entities/PokemongMove.java
rename to src/main/java/fr/uca/iut/entities/denormalized/PokemongMove.java
index 13c4c31..6f0b3d8 100644
--- a/src/main/java/fr/uca/iut/entities/PokemongMove.java
+++ b/src/main/java/fr/uca/iut/entities/denormalized/PokemongMove.java
@@ -1,16 +1,19 @@
-package fr.uca.iut.entities;
-
-public class PokemongMove extends GenericEntity {
-
- private String name;
-
- public PokemongMove() {}
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-}
+package fr.uca.iut.entities.denormalized;
+
+import fr.uca.iut.entities.GenericEntity;
+
+public class PokemongMove extends GenericEntity {
+
+ private String name;
+
+ public PokemongMove() {
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/fr/uca/iut/entities/TrainerPokemong.java b/src/main/java/fr/uca/iut/entities/denormalized/TrainerPokemong.java
similarity index 82%
rename from src/main/java/fr/uca/iut/entities/TrainerPokemong.java
rename to src/main/java/fr/uca/iut/entities/denormalized/TrainerPokemong.java
index f63fcb5..f13a2ee 100644
--- a/src/main/java/fr/uca/iut/entities/TrainerPokemong.java
+++ b/src/main/java/fr/uca/iut/entities/denormalized/TrainerPokemong.java
@@ -1,6 +1,7 @@
-package fr.uca.iut.entities;
+package fr.uca.iut.entities.denormalized;
import com.mongodb.lang.Nullable;
+import fr.uca.iut.entities.GenericEntity;
import fr.uca.iut.utils.enums.PokemongName;
public class TrainerPokemong extends GenericEntity {
@@ -9,7 +10,8 @@ public class TrainerPokemong extends GenericEntity {
private PokemongName species;
- public TrainerPokemong() {}
+ public TrainerPokemong() {
+ }
@Nullable
public String getNickname() {
diff --git a/src/main/java/fr/uca/iut/entities/Type.java b/src/main/java/fr/uca/iut/entities/embedded/Type.java
similarity index 73%
rename from src/main/java/fr/uca/iut/entities/Type.java
rename to src/main/java/fr/uca/iut/entities/embedded/Type.java
index f9e3022..38dc8a7 100644
--- a/src/main/java/fr/uca/iut/entities/Type.java
+++ b/src/main/java/fr/uca/iut/entities/embedded/Type.java
@@ -1,56 +1,56 @@
-package fr.uca.iut.entities;
-
-import fr.uca.iut.utils.enums.TypeName;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
-public class Type {
-
- private TypeName name;
- private List weakAgainst;
- private List effectiveAgainst;
-
- public Type() {}
-
- public TypeName getName() {
- return name;
- }
-
- public void setName(TypeName name) {
- this.name = name;
- }
-
- public List getWeakAgainst() {
- return Collections.unmodifiableList(weakAgainst);
- }
-
- public void setWeakAgainst(List weakAgainst) {
- this.weakAgainst = weakAgainst;
- }
-
- public List getEffectiveAgainst() {
- return Collections.unmodifiableList(effectiveAgainst);
- }
-
- public void setEffectiveAgainst(List effectiveAgainst) {
- this.effectiveAgainst = effectiveAgainst;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, weakAgainst, effectiveAgainst);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Type type = (Type) o;
- return Objects.equals(name, type.name) &&
- Objects.equals(weakAgainst, type.weakAgainst) &&
- Objects.equals(effectiveAgainst, type.effectiveAgainst);
- }
-
+package fr.uca.iut.entities.embedded;
+
+import fr.uca.iut.utils.enums.TypeName;
+
+import java.util.List;
+import java.util.Objects;
+
+public class Type {
+
+ private TypeName name;
+ private List weakAgainst;
+ private List effectiveAgainst;
+
+ public Type() {
+ }
+
+ public TypeName getName() {
+ return name;
+ }
+
+ public void setName(TypeName name) {
+ this.name = name;
+ }
+
+ public List getWeakAgainst() {
+ return weakAgainst;
+ }
+
+ public void setWeakAgainst(List weakAgainst) {
+ this.weakAgainst = weakAgainst;
+ }
+
+ public List getEffectiveAgainst() {
+ return effectiveAgainst;
+ }
+
+ public void setEffectiveAgainst(List effectiveAgainst) {
+ this.effectiveAgainst = effectiveAgainst;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, weakAgainst, effectiveAgainst);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Type type = (Type) o;
+ return Objects.equals(name, type.name) &&
+ Objects.equals(weakAgainst, type.weakAgainst) &&
+ Objects.equals(effectiveAgainst, type.effectiveAgainst);
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/fr/uca/iut/repositories/GenericRepository.java b/src/main/java/fr/uca/iut/repositories/GenericRepository.java
index 649e2a4..71b208a 100644
--- a/src/main/java/fr/uca/iut/repositories/GenericRepository.java
+++ b/src/main/java/fr/uca/iut/repositories/GenericRepository.java
@@ -2,7 +2,9 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
+import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.ReplaceOptions;
+import com.mongodb.client.model.WriteModel;
import com.mongodb.lang.Nullable;
import fr.uca.iut.entities.GenericEntity;
import org.bson.Document;
@@ -27,7 +29,7 @@ public abstract class GenericRepository {
@Nullable
public T findById(String id) {
return getCollection().find(eq("_id", new ObjectId(id)))
- .first();
+ .first();
}
protected abstract MongoCollection getCollection();
@@ -38,7 +40,7 @@ public abstract class GenericRepository {
public List listAll() {
return getCollection().find()
- .into(new ArrayList<>());
+ .into(new ArrayList<>());
}
public void persistOrUpdate(@NotNull T entity) {
@@ -49,6 +51,21 @@ public abstract class GenericRepository {
);
}
+ public void updateAll(@NotNull List entities) {
+ List> updates = new ArrayList<>();
+ for (T entity : entities) {
+ updates.add(
+ new ReplaceOneModel<>(
+ eq("_id", new ObjectId(entity.getId())),
+ entity,
+ new ReplaceOptions().upsert(true)
+ )
+ );
+ }
+
+ getCollection().bulkWrite(updates);
+ }
+
public void delete(@NotNull T entity) {
getCollection().deleteOne(eq("_id", new ObjectId(entity.getId())));
}
diff --git a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java
index 3da2595..6e3c634 100644
--- a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java
+++ b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java
@@ -33,7 +33,7 @@ public class PokemongRepository extends GenericRepository {
public List findByMove(String moveId) {
Bson filter = Filters.elemMatch("moveSet", Filters.eq("_id", new ObjectId(moveId)));
return getCollection().find(filter)
- .into(new ArrayList<>());
+ .into(new ArrayList<>());
}
@Override
diff --git a/src/main/java/fr/uca/iut/services/GenericService.java b/src/main/java/fr/uca/iut/services/GenericService.java
index 3bd1435..e3bdb41 100644
--- a/src/main/java/fr/uca/iut/services/GenericService.java
+++ b/src/main/java/fr/uca/iut/services/GenericService.java
@@ -17,10 +17,20 @@ public abstract class GenericService {
}
public T addOne(@NotNull T entity) {
+ validateOne(entity);
repository.persist(entity);
return entity;
}
+ /**
+ * Override me and start with `super.validateOne(entity);`
+ */
+ public void validateOne(T entity) throws NonValidEntityException {
+ if (entity == null) {
+ throw new NonValidEntityException("entity was null");
+ }
+ }
+
@Nullable
public T getOneById(String id) {
return repository.findById(id);
@@ -37,15 +47,21 @@ public abstract class GenericService {
}
}
- @Nullable
- public abstract T updateOne(@NotNull T entity);
-
/**
- * Override me and start with `super.validateOne(entity);`
+ * Override me
*/
- public void validateOne(T entity) {
- if (entity == null) {
- throw new NonValidEntityException("entity was null");
+ @Nullable
+ public T updateOne(@NotNull T entity) {
+ validateOne(entity);
+ return entity;
+ }
+
+ public void updateAll(List entities) {
+ if (!entities.isEmpty()) {
+ for (T entity : entities) {
+ validateOne(entity);
+ }
+ repository.updateAll(entities);
}
}
}
diff --git a/src/main/java/fr/uca/iut/services/MoveService.java b/src/main/java/fr/uca/iut/services/MoveService.java
index ed30c0e..590de05 100644
--- a/src/main/java/fr/uca/iut/services/MoveService.java
+++ b/src/main/java/fr/uca/iut/services/MoveService.java
@@ -13,6 +13,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
@ApplicationScoped
public class MoveService extends GenericService {
@@ -27,41 +29,6 @@ public class MoveService extends GenericService {
setRepository(moveRepository);
}
- @Override
- public void deleteOneById(String id) {
- super.deleteOneById(id);
- List pokemongs = pokemongService.findByMove(id);
- for (Pokemong pokemong : pokemongs) {
- pokemong.removeMove(id);
- pokemongService.updateOne(pokemong);
- }
- }
-
- @Override
- @Nullable
- public Move updateOne(@NotNull Move move) {
- Move existingMove = moveRepository.findById(move.getId());
- if (existingMove != null) {
- if (!existingMove.getName()
- .equals(move.getName()))
- {
- existingMove.setName(move.getName());
- List pokemongs = pokemongService.findByMove(move.getId());
- for (Pokemong pokemong : pokemongs) {
- pokemong.updateMove(move.getId(), move.getName());
- pokemongService.updateOne(pokemong);
- }
- }
-
- existingMove.setPower(move.getPower());
- existingMove.setCategory(move.getCategory());
- existingMove.setAccuracy(move.getAccuracy());
- existingMove.setType(move.getType());
- moveRepository.persistOrUpdate(existingMove);
- }
- return existingMove;
- }
-
@Override
public void validateOne(Move move) {
@@ -89,11 +56,87 @@ public class MoveService extends GenericService {
errors.add("move type was null or invalid");
}
+ if (move.getSchemaVersion() == null || !Objects.equals(move.getSchemaVersion(), Move.LATEST_SCHEMA_VERSION)) {
+ errors.add("move schema version was null or not the latest version: " + Move.LATEST_SCHEMA_VERSION);
+ }
+
if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
}
}
+ @Nullable
+ @Override
+ public Move getOneById(String id) {
+ return migrateToV2(super.getOneById(id));
+ }
+
+ @Override
+ public List getAll() {
+ return super.getAll()
+ .stream()
+ .map(this::migrateToV2)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void deleteOneById(String id) {
+ List pokemongs = pokemongService.findByMove(id);
+ List pokemongsToUpdate = new ArrayList<>();
+ for (Pokemong pokemong : pokemongs) {
+ pokemong.removeMove(id);
+ pokemongsToUpdate.add(pokemong);
+ }
+ pokemongService.updateAll(pokemongsToUpdate);
+ super.deleteOneById(id);
+ }
+
+ @Override
+ @Nullable
+ public Move updateOne(@NotNull Move move) {
+ super.updateOne(move);
+ Move existingMove = moveRepository.findById(move.getId());
+ if (existingMove != null) {
+ if (!existingMove.getName().equals(move.getName())) {
+ existingMove.setName(move.getName());
+ batchUpdatePokemongTrainers(move);
+ }
+
+ existingMove.setPower(move.getPower());
+ existingMove.setCategory(move.getCategory());
+ existingMove.setAccuracy(move.getAccuracy());
+ existingMove.setType(move.getType());
+ moveRepository.persistOrUpdate(existingMove);
+ }
+ return existingMove;
+ }
+
+ private void batchUpdatePokemongTrainers(@NotNull Move move) {
+ List pokemongs = pokemongService.findByMove(move.getId());
+ List pokemongsToUpdate = new ArrayList<>();
+ for (Pokemong pokemong : pokemongs) {
+ pokemong.updateMove(move.getId(), move.getName());
+ pokemongsToUpdate.add(pokemong);
+ }
+ pokemongService.updateAll(pokemongsToUpdate);
+ }
+
+ /**
+ * We want to migrate the documents incrementally, so we upgrade the
+ * schema version if it is less than the current schema version,
+ * and then save the updated document back to the database.
+ *
+ * @param move the Move found by the repository
+ * @return the Move(V2) based on the Move from the repository
+ */
+ private Move migrateToV2(Move move) {
+ if (move != null && move.getSchemaVersion() < 2) {
+ move.setSchemaVersion(2);
+ moveRepository.persistOrUpdate(move);
+ }
+ return move;
+ }
+
public boolean existsById(String moveId) {
return moveRepository.existsById(moveId);
}
diff --git a/src/main/java/fr/uca/iut/services/PokemongService.java b/src/main/java/fr/uca/iut/services/PokemongService.java
index ae9afd9..a3c0743 100644
--- a/src/main/java/fr/uca/iut/services/PokemongService.java
+++ b/src/main/java/fr/uca/iut/services/PokemongService.java
@@ -1,7 +1,11 @@
package fr.uca.iut.services;
import com.mongodb.lang.Nullable;
-import fr.uca.iut.entities.*;
+import fr.uca.iut.entities.Pokemong;
+import fr.uca.iut.entities.Trainer;
+import fr.uca.iut.entities.denormalized.PokemongMove;
+import fr.uca.iut.entities.denormalized.TrainerPokemong;
+import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.repositories.PokemongRepository;
import fr.uca.iut.utils.StringUtils;
import fr.uca.iut.utils.enums.PokemongName;
@@ -36,81 +40,19 @@ public class PokemongService extends GenericService {
@Override
public Pokemong addOne(@NotNull Pokemong pokemong) {
Pokemong persistedPokemong = super.addOne(pokemong);
-
- Trainer trainer = trainerService.getOneById(pokemong.getTrainer());
- if (trainer != null) {
- TrainerPokemong trainerPokemong = new TrainerPokemong();
- trainerPokemong.setId(pokemong.getId());
- trainerPokemong.setNickname(pokemong.getNickname());
- trainerPokemong.setSpecies(pokemong.getSpecies());
- trainer.getPokemongs()
- .add(trainerPokemong);
- trainerService.updateOne(trainer);
- }
- return persistedPokemong;
- }
-
- @Override
- public void deleteOneById(String id) {
- Pokemong pokemong = getOneById(id);
- if (pokemong != null && pokemong.getTrainer() != null) {
+ String trainerId = pokemong.getTrainer();
+ if (trainerId != null) {
Trainer trainer = trainerService.getOneById(pokemong.getTrainer());
if (trainer != null) {
- trainer.getPokemongs()
- .removeIf(trainerPokemong -> trainerPokemong.getId()
- .equals(id));
+ TrainerPokemong trainerPokemong = new TrainerPokemong();
+ trainerPokemong.setId(pokemong.getId());
+ trainerPokemong.setNickname(pokemong.getNickname());
+ trainerPokemong.setSpecies(pokemong.getSpecies());
+ trainer.addPokemong(trainerPokemong);
trainerService.updateOne(trainer);
}
}
- super.deleteOneById(id);
- }
-
- @Override
- @Nullable
- public Pokemong updateOne(@NotNull Pokemong pokemong) {
- Pokemong existingPokemong = pokemongRepository.findById(pokemong.getId());
- if (existingPokemong != null) {
- boolean nicknameChanged = !Objects.equals(existingPokemong.getNickname(), pokemong.getNickname());
- boolean evoStageChanged = !Objects.equals(existingPokemong.getEvoStage(), pokemong.getEvoStage());
- boolean evoTrackChanged = !Objects.equals(existingPokemong.getEvoTrack(), pokemong.getEvoTrack());
-
- existingPokemong.setNickname(pokemong.getNickname());
- existingPokemong.setDob(pokemong.getDob());
- existingPokemong.setLevel(pokemong.getLevel());
- existingPokemong.setPokedexId(pokemong.getPokedexId());
- existingPokemong.setEvoStage(pokemong.getEvoStage());
- existingPokemong.setEvoTrack(pokemong.getEvoTrack());
- existingPokemong.setTrainer(pokemong.getTrainer());
- existingPokemong.setTypes(pokemong.getTypes());
- existingPokemong.setMoveSet(pokemong.getMoveSet());
-
- pokemongRepository.persistOrUpdate(existingPokemong);
-
- if (nicknameChanged || evoStageChanged || evoTrackChanged) {
- Trainer trainer = trainerService.getOneById(existingPokemong.getTrainer());
- if (trainer != null) {
- TrainerPokemong trainerPokemong = trainer.getPokemongs()
- .stream()
- .filter(tp -> tp.getId()
- .equals(existingPokemong.getId()))
- .findFirst()
- .orElse(null);
-
- if (trainerPokemong != null) {
- if (nicknameChanged) {
- trainerPokemong.setNickname(existingPokemong.getNickname());
- }
-
- if (evoStageChanged || evoTrackChanged) {
- trainerPokemong.setSpecies(existingPokemong.getSpecies());
- }
-
- trainerService.updateOne(trainer);
- }
- }
- }
- }
- return existingPokemong;
+ return persistedPokemong;
}
@Override
@@ -140,19 +82,17 @@ public class PokemongService extends GenericService {
errors.add("pokemong evo track was null or invalid");
}
- List types = pokemong.getTypes();
+ Set types = pokemong.getTypes();
if (types == null
- || types.size() == 0
- || types.size() > 2)
- {
+ || types.size() == 0
+ || types.size() > 2) {
errors.add("pokemong types was null or empty or had more than 2 types");
}
Set moveSet = pokemong.getMoveSet();
if (moveSet == null) {
errors.add("pokemong move set was null");
- }
- else {
+ } else {
if (moveSet.size() == 0 || moveSet.size() > 4) {
errors.add("pokemong move set was empty or had more than 4 moves");
}
@@ -171,11 +111,88 @@ public class PokemongService extends GenericService {
}
}
+ if (pokemong.getSchemaVersion() == null ||
+ !Objects.equals(pokemong.getSchemaVersion(), Pokemong.LATEST_SCHEMA_VERSION)) {
+ errors.add(
+ "pokemong schema version was null or not the latest version: " + Pokemong.LATEST_SCHEMA_VERSION);
+ }
+
if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
}
}
+ @Override
+ public void deleteOneById(String id) {
+ Pokemong pokemong = getOneById(id);
+ if (pokemong != null && pokemong.getTrainer() != null) {
+ Trainer trainer = trainerService.getOneById(pokemong.getTrainer());
+ if (trainer != null) {
+ trainer.removePokemong(id);
+ trainerService.updateOne(trainer);
+ }
+ }
+ super.deleteOneById(id);
+ }
+
+ @Override
+ @Nullable
+ public Pokemong updateOne(@NotNull Pokemong pokemong) {
+ super.updateOne(pokemong);
+ Pokemong existingPokemong = pokemongRepository.findById(pokemong.getId());
+ if (existingPokemong != null) {
+ boolean nicknameChanged = !Objects.equals(existingPokemong.getNickname(), pokemong.getNickname());
+ boolean evoStageChanged = !Objects.equals(existingPokemong.getEvoStage(), pokemong.getEvoStage());
+ boolean evoTrackChanged = !Objects.equals(existingPokemong.getEvoTrack(), pokemong.getEvoTrack());
+
+ existingPokemong.setNickname(pokemong.getNickname());
+ existingPokemong.setDob(pokemong.getDob());
+ existingPokemong.setLevel(pokemong.getLevel());
+ existingPokemong.setPokedexId(pokemong.getPokedexId());
+ existingPokemong.setEvoStage(pokemong.getEvoStage());
+ existingPokemong.setEvoTrack(pokemong.getEvoTrack());
+ existingPokemong.setTrainer(pokemong.getTrainer());
+ existingPokemong.setTypes(pokemong.getTypes());
+ existingPokemong.setMoveSet(pokemong.getMoveSet());
+
+ pokemongRepository.persistOrUpdate(existingPokemong);
+
+ if (nicknameChanged || evoStageChanged || evoTrackChanged) {
+ updateTrainerPokemong(existingPokemong, nicknameChanged, evoStageChanged, evoTrackChanged);
+ }
+ }
+ return existingPokemong;
+ }
+
+ private void updateTrainerPokemong(
+ @NotNull Pokemong existingPokemong,
+ boolean nicknameChanged,
+ boolean evoStageChanged,
+ boolean evoTrackChanged
+ ) {
+ Trainer trainer = trainerService.getOneById(existingPokemong.getTrainer());
+ if (trainer != null) {
+ TrainerPokemong trainerPokemong =
+ trainer.getPokemongs()
+ .stream()
+ .filter(tp -> tp.getId()
+ .equals(existingPokemong.getId()))
+ .findFirst()
+ .orElse(null);
+
+ if (trainerPokemong != null) {
+ if (nicknameChanged) {
+ trainerPokemong.setNickname(existingPokemong.getNickname());
+ }
+
+ if (evoStageChanged || evoTrackChanged) {
+ trainerPokemong.setSpecies(existingPokemong.getSpecies());
+ }
+ trainerService.updateOne(trainer);
+ }
+ }
+ }
+
public List findByMove(String id) {
return pokemongRepository.findByMove(id);
}
@@ -190,13 +207,16 @@ public class PokemongService extends GenericService {
return repository.existsById(pokemongId);
}
- public void batchUpdatePokemongTrainers(List trainerPokemongs, @Nullable String trainerId) {
+ public void batchUpdatePokemongTrainers(@NotNull Set trainerPokemongs,
+ @Nullable String trainerId) {
+ List pokemongsToUpdate = new ArrayList<>();
for (TrainerPokemong trainerPokemong : trainerPokemongs) {
Pokemong pokemong = getOneById(trainerPokemong.getId());
- if (pokemong != null) {
+ if (pokemong != null && !Objects.equals(pokemong.getTrainer(), trainerId)) {
pokemong.setTrainer(trainerId);
- updateOne(pokemong);
+ pokemongsToUpdate.add(pokemong);
}
}
+ updateAll(pokemongsToUpdate);
}
}
diff --git a/src/main/java/fr/uca/iut/services/TrainerService.java b/src/main/java/fr/uca/iut/services/TrainerService.java
index 65d8a88..07724ac 100644
--- a/src/main/java/fr/uca/iut/services/TrainerService.java
+++ b/src/main/java/fr/uca/iut/services/TrainerService.java
@@ -3,7 +3,7 @@ package fr.uca.iut.services;
import com.mongodb.lang.Nullable;
import fr.uca.iut.entities.Pokemong;
import fr.uca.iut.entities.Trainer;
-import fr.uca.iut.entities.TrainerPokemong;
+import fr.uca.iut.entities.denormalized.TrainerPokemong;
import fr.uca.iut.repositories.TrainerRepository;
import fr.uca.iut.utils.StringUtils;
import fr.uca.iut.utils.exceptions.NonValidEntityException;
@@ -12,8 +12,8 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
@ApplicationScoped
public class TrainerService extends GenericService {
@@ -33,38 +33,14 @@ public class TrainerService extends GenericService {
public Trainer addOne(@NotNull Trainer trainer) {
Trainer persistedTrainer = super.addOne(trainer);
+ // If this trainer gained pokemongs, that pokemong's ex-trainer if any needs to lose said pokemongs
+ transferNewlyArrivedTrainerPokemongs(new HashSet<>(), persistedTrainer.getPokemongs());
+ // all owned pokemongs gain this trainer's reference
pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), trainer.getId());
return persistedTrainer;
}
- @Override
- public void deleteOneById(String id) {
- Trainer trainer = getOneById(id);
-
- if (trainer != null) {
- pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), null);
- }
-
- super.deleteOneById(id);
- }
-
- @Nullable
- @Override
- public Trainer updateOne(@NotNull Trainer trainer) {
- Trainer existingTrainer = trainerRepository.findById(trainer.getId());
- if (existingTrainer != null) {
- existingTrainer.setName(trainer.getName());
- existingTrainer.setDob(trainer.getDob());
- existingTrainer.setWins(trainer.getLosses());
- existingTrainer.setLosses(trainer.getLosses());
- existingTrainer.setPastOpponents(trainer.getPastOpponents());
- existingTrainer.setPokemongs(trainer.getPokemongs());
- trainerRepository.persistOrUpdate(existingTrainer);
- }
- return existingTrainer;
- }
-
@Override
public void validateOne(Trainer trainer) {
@@ -92,45 +68,118 @@ public class TrainerService extends GenericService {
if (pastOpponents == null) {
errors.add("trainer past opponents collection was null");
- }
- else {
+ } else {
for (String trainerId : pastOpponents) {
- if (StringUtils.isBlankStringOrNull(trainerId) || !trainerRepository.existsById(trainerId)) {
- errors.add("trainer past opponents collection contained an invalid or unknown id");
+ if (StringUtils.isBlankStringOrNull(trainerId)) {
+ errors.add("trainer past opponents collection contained an invalid id: " + trainerId);
}
}
}
- List pokemongs = trainer.getPokemongs();
+ Set pokemongs = trainer.getPokemongs();
if (pokemongs == null) {
errors.add("trainer pokemongs collection was null or invalid");
- }
- else {
+ } else {
for (TrainerPokemong pokemong : pokemongs) {
String pokemongId = pokemong.getId();
if (StringUtils.isBlankStringOrNull(pokemongId) || !pokemongService.existsById(pokemongId)) {
errors.add("pokemong with id " + pokemongId + " does not exist");
- }
- else {
+ } else {
if (!pokemongService.isEvoValid(pokemongId, pokemong.getSpecies())) {
errors.add("pokemong with id " + pokemongId + " cannot be a " +
- pokemong.getSpecies());
+ pokemong.getSpecies());
}
Pokemong pokemongBehind = pokemongService.getOneById(pokemongId);
if (pokemong.getNickname() != null
- && pokemongBehind != null
- && !pokemong.getNickname()
- .equals(pokemongBehind.getNickname()))
- {
+ && pokemongBehind != null
+ && !pokemong.getNickname()
+ .equals(pokemongBehind.getNickname())) {
errors.add("pokemong with id " + pokemongId + " already has a nickname");
}
}
}
}
+ if (trainer.getSchemaVersion() == null ||
+ !Objects.equals(trainer.getSchemaVersion(), Trainer.LATEST_SCHEMA_VERSION)) {
+ errors.add("trainer schema version was null or not the latest version: " + Trainer.LATEST_SCHEMA_VERSION);
+ }
+
if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
}
}
+
+ @Override
+ public void deleteOneById(String id) {
+ Trainer trainer = getOneById(id);
+
+ if (trainer != null) {
+ pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), null);
+ }
+
+ super.deleteOneById(id);
+ }
+
+ @Nullable
+ @Override
+ public Trainer updateOne(@NotNull Trainer trainer) {
+ super.updateOne(trainer);
+ Trainer existingTrainer = trainerRepository.findById(trainer.getId());
+ if (existingTrainer != null) {
+ Set oldPokemongs = existingTrainer.getPokemongs();
+
+ existingTrainer.setName(trainer.getName());
+ existingTrainer.setDob(trainer.getDob());
+ existingTrainer.setWins(trainer.getLosses());
+ existingTrainer.setLosses(trainer.getLosses());
+ existingTrainer.setPastOpponents(trainer.getPastOpponents());
+ existingTrainer.setPokemongs(trainer.getPokemongs());
+ trainerRepository.persistOrUpdate(existingTrainer);
+
+ Set newPokemongs = trainer.getPokemongs();
+
+ // all old pokemongs who are not there anymore lose their trainer reference
+ pokemongService.batchUpdatePokemongTrainers(
+ oldPokemongs.stream()
+ .filter(tp -> !newPokemongs.contains(tp))
+ .collect(Collectors.toSet()),
+ null);
+ // If this trainer gained a pokemong, that pokemong's ex-trainer if any needs to lose said pokemong
+ transferNewlyArrivedTrainerPokemongs(oldPokemongs, newPokemongs);
+ // all new pokemongs who were not there before gain this trainer's reference
+ pokemongService.batchUpdatePokemongTrainers(
+ newPokemongs.stream()
+ .filter(tp -> !oldPokemongs.contains(tp))
+ .collect(Collectors.toSet()),
+ existingTrainer.getId());
+ }
+ return existingTrainer;
+ }
+
+ private void transferNewlyArrivedTrainerPokemongs(
+ @NotNull Set oldPokemongs,
+ @NotNull Set newPokemongs
+ ) {
+ List trainersToUpdate = new ArrayList<>();
+
+ for (TrainerPokemong newTrainerPokemong : newPokemongs) {
+ if (oldPokemongs.isEmpty() || !oldPokemongs.contains(newTrainerPokemong)) {
+ Pokemong pokemong = pokemongService.getOneById(newTrainerPokemong.getId());
+ if (pokemong != null) {
+ String oldTrainerId = pokemong.getTrainer();
+ // If the pokemong already had a trainer, remove it from the old trainer's pokemongs list
+ if (oldTrainerId != null) {
+ Trainer oldTrainer = getOneById(oldTrainerId);
+ if (oldTrainer != null) {
+ oldTrainer.removePokemong(newTrainerPokemong.getId());
+ trainersToUpdate.add(oldTrainer);
+ }
+ }
+ }
+ }
+ }
+ updateAll(trainersToUpdate);
+ }
}
diff --git a/src/main/resources/META-INF/openapi.yaml b/src/main/resources/META-INF/openapi.yaml
index b3192f0..158ad35 100644
--- a/src/main/resources/META-INF/openapi.yaml
+++ b/src/main/resources/META-INF/openapi.yaml
@@ -310,6 +310,7 @@ components:
- evoTrack
- types
- moveSet
+ - schemaVersion
properties:
nickname:
type: string
@@ -347,6 +348,8 @@ components:
maxItems: 4
items:
$ref: '#/components/schemas/PokemongMove'
+ schemaVersion:
+ $ref: '#/components/schemas/SchemaVersion'
Move:
type: object
@@ -356,6 +359,7 @@ components:
- category
- accuracy
- type
+ - schemaVersion
properties:
name:
type: string
@@ -370,6 +374,8 @@ components:
minimum: 0
type:
$ref: '#/components/schemas/TypeName'
+ schemaVersion:
+ $ref: '#/components/schemas/SchemaVersion'
Trainer:
type: object
@@ -380,6 +386,7 @@ components:
- losses
- pastOpponents
- pokemongs
+ - schemaVersion
properties:
name:
type: string
@@ -402,6 +409,8 @@ components:
type: array
items:
$ref: '#/components/schemas/TrainerPokemong'
+ schemaVersion:
+ $ref: '#/components/schemas/SchemaVersion'
PokemongMove:
type: object
@@ -431,6 +440,11 @@ components:
species:
$ref: '#/components/schemas/PokemongName'
+ SchemaVersion:
+ type: integer
+ minimum: 1
+ description: must be >= 1, and furthermore must be == latest schema version
+
MoveCategoryName:
type: string
enum: [