📍 Implement Schema Versioning NoSQL pattern (#5)

Co-authored-by: alexis.drai@etu.uca.fr <alexis.drai@etu.uca.fr>
Reviewed-on: alexis.drai/AD_MongoDB#5
Alexis Drai 2 years ago committed by alexis.drai@etu.uca.fr
parent 6565b12586
commit b92d9a19ef

1
.gitignore vendored

@ -85,3 +85,4 @@ gradle-app.setting
# Others # Others
docs/todos.md docs/todos.md
/src/test/resources/application.properties /src/test/resources/application.properties
/docs/sample-dataset/load_data.sh

@ -6,19 +6,71 @@ Instructions are [here](https://clientserveur-courses.clubinfo-clermont.fr/Notat
## About ## 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 This application is a RESTful service designed to emulate a basic `Pokemong` management system. It allows users to
CRUD operations on Pokemongs, trainers, moves, and types. perform
CRUD operations on `Pokemongs`, `Trainers`, `Moves`, and `Types`.
### 🗂️ DCM ### 🗂️ DCM
<img src="./docs/mcd.png" alt="Data Concept Model" title="Data Concept Model"> <img src="./docs/mcd.png" alt="Data Concept Model" title="Data Concept Model">
### 🧬 UML Class diagram ### 🧬 UML Class diagram
<img src="./docs/nosql_uml.png" alt="UML Class Diagram" title="UML Class Diagram"> <img src="./docs/nosql_uml.png" alt="UML Class Diagram" title="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 ## Prep steps
### ♨️ Java version ### ♨️ Java version
@ -79,19 +131,30 @@ You can run the application in dev mode using:
## API testing ## 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://<username>:<password>@<cluster>.<node>.mongodb.net/<databasename> --collection=moves --file=./docs/sample-dataset/moves.json
```
### 🏴‍☠️ SwaggerUI ### 🏴‍☠️ SwaggerUI
Thanks to this project's OpenAPI specs, you can explore the API in a lot of ways. 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. 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 ⚠️ 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 ### 🩺 API testing tools
You can use an API testing tool such as [Postman](https://www.postman.com/) You can use an API testing tool such as [Postman](https://www.postman.com/)
or [Insomnia](https://insomnia.rest/) to test this app. 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) ### 📱 Front end (later)
Moving forward, the front end part of this app -- a different project -- might also come into play for trying out this Moving forward, the front end part of this app -- a different project -- might also come into play for trying out this

@ -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": []
}
]
}
]
}

@ -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"]
}
}

@ -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"
}
]
}

@ -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"
}
]
}

@ -9,6 +9,7 @@ import org.bson.codecs.CollectibleCodec;
import org.bson.codecs.DecoderContext; import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext; import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
public abstract class GenericCodec<T extends GenericEntity> implements CollectibleCodec<T> { public abstract class GenericCodec<T extends GenericEntity> implements CollectibleCodec<T> {
@ -27,12 +28,12 @@ public abstract class GenericCodec<T extends GenericEntity> implements Collectib
} }
@Override @Override
public boolean documentHasId(T document) { public boolean documentHasId(@NotNull T document) {
return document.getId() != null; return document.getId() != null;
} }
@Override @Override
public BsonValue getDocumentId(T document) { public BsonValue getDocumentId(@NotNull T document) {
return new BsonObjectId(new ObjectId(document.getId())); return new BsonObjectId(new ObjectId(document.getId()));
} }

@ -4,7 +4,7 @@ import com.mongodb.MongoClientSettings;
import fr.uca.iut.codecs.GenericCodec; import fr.uca.iut.codecs.GenericCodec;
import fr.uca.iut.codecs.type.TypeCodecUtil; import fr.uca.iut.codecs.type.TypeCodecUtil;
import fr.uca.iut.entities.Move; import fr.uca.iut.entities.Move;
import fr.uca.iut.entities.Type; import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.utils.enums.MoveCategoryName; import fr.uca.iut.utils.enums.MoveCategoryName;
import org.bson.BsonReader; import org.bson.BsonReader;
import org.bson.BsonWriter; import org.bson.BsonWriter;
@ -13,21 +13,24 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext; import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext; import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
public class MoveCodec extends GenericCodec<Move> { public class MoveCodec extends GenericCodec<Move> {
private final Codec<Document> documentCodec; private final Codec<Document> documentCodec;
public MoveCodec() { public MoveCodec() {
this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() this.documentCodec = MongoClientSettings.getDefaultCodecRegistry()
.get(Document.class); .get(Document.class);
} }
@Override @Override
public void encode(BsonWriter writer, Move move, EncoderContext encoderContext) { public void encode(BsonWriter writer, @NotNull Move move, EncoderContext encoderContext) {
Document doc = new Document(); Document doc = new Document();
doc.put("_id", new ObjectId(move.getId())); doc.put("_id", new ObjectId(move.getId()));
doc.put("schemaVersion", move.getSchemaVersion());
doc.put("name", move.getName()); doc.put("name", move.getName());
doc.put("category", move.getCategory()); doc.put("category", move.getCategory());
@ -39,8 +42,8 @@ public class MoveCodec extends GenericCodec<Move> {
Type moveType = move.getType(); Type moveType = move.getType();
Document typeDoc = new Document(); Document typeDoc = new Document();
typeDoc.put("name", typeDoc.put("name",
moveType.getName() moveType.getName()
.toString()); .toString());
typeDoc.put("weakAgainst", moveType.getWeakAgainst()); typeDoc.put("weakAgainst", moveType.getWeakAgainst());
typeDoc.put("effectiveAgainst", moveType.getEffectiveAgainst()); typeDoc.put("effectiveAgainst", moveType.getEffectiveAgainst());
doc.put("type", typeDoc); doc.put("type", typeDoc);
@ -55,11 +58,25 @@ public class MoveCodec extends GenericCodec<Move> {
@Override @Override
public Move decode(BsonReader reader, DecoderContext decoderContext) { public Move decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, 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 move = new Move();
move.setId(document.getObjectId("_id") move.setId(document.getObjectId("_id")
.toString()); .toString());
move.setSchemaVersion(document.getInteger("schemaVersion"));
move.setName(document.getString("name")); move.setName(document.getString("name"));
@ -73,6 +90,33 @@ public class MoveCodec extends GenericCodec<Move> {
move.setType(TypeCodecUtil.extractType(typeDoc)); move.setType(TypeCodecUtil.extractType(typeDoc));
// Read and discard the old pp field
Integer pp = document.getInteger("pp");
return move; 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;
}
} }

@ -4,8 +4,8 @@ import com.mongodb.MongoClientSettings;
import fr.uca.iut.codecs.GenericCodec; import fr.uca.iut.codecs.GenericCodec;
import fr.uca.iut.codecs.type.TypeCodecUtil; import fr.uca.iut.codecs.type.TypeCodecUtil;
import fr.uca.iut.entities.Pokemong; import fr.uca.iut.entities.Pokemong;
import fr.uca.iut.entities.PokemongMove; import fr.uca.iut.entities.denormalized.PokemongMove;
import fr.uca.iut.entities.Type; import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.utils.enums.PokemongName; import fr.uca.iut.utils.enums.PokemongName;
import org.bson.BsonReader; import org.bson.BsonReader;
import org.bson.BsonWriter; import org.bson.BsonWriter;
@ -14,6 +14,7 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext; import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext; import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Date; import java.util.Date;
@ -26,21 +27,23 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
public PokemongCodec() { public PokemongCodec() {
this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() this.documentCodec = MongoClientSettings.getDefaultCodecRegistry()
.get(Document.class); .get(Document.class);
} }
@Override @Override
public void encode(BsonWriter writer, Pokemong pokemong, EncoderContext encoderContext) { public void encode(BsonWriter writer, @NotNull Pokemong pokemong, EncoderContext encoderContext) {
Document doc = new Document(); Document doc = new Document();
doc.put("_id", new ObjectId(pokemong.getId())); doc.put("_id", new ObjectId(pokemong.getId()));
doc.put("schemaVersion", pokemong.getSchemaVersion());
doc.put("nickname", pokemong.getNickname()); doc.put("nickname", pokemong.getNickname());
doc.put("dob", doc.put("dob",
Date.from(pokemong.getDob() Date.from(pokemong.getDob()
.atStartOfDay(ZoneId.systemDefault()) .atStartOfDay(ZoneId.systemDefault())
.toInstant())); .toInstant()));
doc.put("level", pokemong.getLevel()); doc.put("level", pokemong.getLevel());
@ -49,9 +52,9 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
doc.put("evoStage", pokemong.getEvoStage()); doc.put("evoStage", pokemong.getEvoStage());
List<String> evoTrack = pokemong.getEvoTrack() List<String> evoTrack = pokemong.getEvoTrack()
.stream() .stream()
.map(Enum::name) .map(Enum::name)
.collect(Collectors.toList()); .collect(Collectors.toList());
doc.put("evoTrack", evoTrack); doc.put("evoTrack", evoTrack);
if (pokemong.getTrainer() != null) { if (pokemong.getTrainer() != null) {
@ -59,36 +62,36 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
} }
List<Document> types = pokemong.getTypes() List<Document> types = pokemong.getTypes()
.stream() .stream()
.map(type -> { .map(type -> {
Document typeDoc = new Document(); Document typeDoc = new Document();
typeDoc.put("name", typeDoc.put("name",
type.getName() type.getName()
.name()); .name());
List<String> weakAgainst = type.getWeakAgainst() List<String> weakAgainst = type.getWeakAgainst()
.stream() .stream()
.map(Enum::name) .map(Enum::name)
.collect(Collectors.toList()); .collect(Collectors.toList());
typeDoc.put("weakAgainst", weakAgainst); typeDoc.put("weakAgainst", weakAgainst);
List<String> effectiveAgainst = type.getEffectiveAgainst() List<String> effectiveAgainst = type.getEffectiveAgainst()
.stream() .stream()
.map(Enum::name) .map(Enum::name)
.collect(Collectors.toList()); .collect(Collectors.toList());
typeDoc.put("effectiveAgainst", effectiveAgainst); typeDoc.put("effectiveAgainst", effectiveAgainst);
return typeDoc; return typeDoc;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
doc.put("types", types); doc.put("types", types);
List<Document> moveSetDocs = pokemong.getMoveSet() List<Document> moveSetDocs = pokemong.getMoveSet()
.stream() .stream()
.map(move -> { .map(move -> {
Document moveDoc = new Document(); Document moveDoc = new Document();
moveDoc.put("_id", new ObjectId(move.getId())); moveDoc.put("_id", new ObjectId(move.getId()));
moveDoc.put("name", move.getName()); moveDoc.put("name", move.getName());
return moveDoc; return moveDoc;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
doc.put("moveSet", moveSetDocs); doc.put("moveSet", moveSetDocs);
documentCodec.encode(writer, doc, encoderContext); documentCodec.encode(writer, doc, encoderContext);
@ -102,18 +105,30 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
@Override @Override
public Pokemong decode(BsonReader reader, DecoderContext decoderContext) { public Pokemong decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, 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 pokemong = new Pokemong();
pokemong.setId(document.getObjectId("_id") pokemong.setId(document.getObjectId("_id")
.toString()); .toString());
pokemong.setSchemaVersion(document.getInteger("schemaVersion"));
pokemong.setNickname(document.getString("nickname")); pokemong.setNickname(document.getString("nickname"));
Date dob = document.getDate("dob"); Date dob = document.getDate("dob");
if (dob != null) { if (dob != null) {
pokemong.setDob(dob.toInstant() pokemong.setDob(dob.toInstant()
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
.toLocalDate()); .toLocalDate());
} }
pokemong.setLevel(document.getInteger("level")); pokemong.setLevel(document.getInteger("level"));
@ -123,9 +138,9 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
pokemong.setEvoStage(document.getInteger("evoStage")); pokemong.setEvoStage(document.getInteger("evoStage"));
List<PokemongName> evoTrack = document.getList("evoTrack", String.class) List<PokemongName> evoTrack = document.getList("evoTrack", String.class)
.stream() .stream()
.map(PokemongName::valueOf) .map(PokemongName::valueOf)
.collect(Collectors.toList()); .collect(Collectors.toList());
pokemong.setEvoTrack(evoTrack); pokemong.setEvoTrack(evoTrack);
ObjectId trainerId = document.getObjectId("trainer"); ObjectId trainerId = document.getObjectId("trainer");
@ -133,21 +148,21 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
pokemong.setTrainer(trainerId.toString()); pokemong.setTrainer(trainerId.toString());
} }
List<Type> types = document.getList("types", Document.class) Set<Type> types = document.getList("types", Document.class)
.stream() .stream()
.map(TypeCodecUtil::extractType) .map(TypeCodecUtil::extractType)
.collect(Collectors.toList()); .collect(Collectors.toSet());
pokemong.setTypes(types); pokemong.setTypes(types);
Set<PokemongMove> moveSet = document.getList("moveSet", Document.class) Set<PokemongMove> moveSet = document.getList("moveSet", Document.class)
.stream() .stream()
.map(pokemongMoveDoc -> { .map(pokemongMoveDoc -> {
PokemongMove move = new PokemongMove(); PokemongMove move = new PokemongMove();
move.setId(((ObjectId) pokemongMoveDoc.get("_id")).toString()); move.setId(((ObjectId) pokemongMoveDoc.get("_id")).toString());
move.setName(pokemongMoveDoc.getString("name")); move.setName(pokemongMoveDoc.getString("name"));
return move; return move;
}) })
.collect(Collectors.toSet()); .collect(Collectors.toSet());
pokemong.setMoveSet(moveSet); pokemong.setMoveSet(moveSet);
return pokemong; return pokemong;

@ -5,11 +5,12 @@ import fr.uca.iut.entities.Pokemong;
import org.bson.codecs.Codec; import org.bson.codecs.Codec;
import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.configuration.CodecRegistry;
import org.jetbrains.annotations.NotNull;
public class PokemongCodecProvider implements CodecProvider { public class PokemongCodecProvider implements CodecProvider {
@Nullable @Nullable
@Override @Override
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) { public <T> Codec<T> get(@NotNull Class<T> clazz, CodecRegistry registry) {
if (clazz.equals(Pokemong.class)) { if (clazz.equals(Pokemong.class)) {
return (Codec<T>) new PokemongCodec(); return (Codec<T>) new PokemongCodec();
} }

@ -3,7 +3,7 @@ package fr.uca.iut.codecs.trainer;
import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings;
import fr.uca.iut.codecs.GenericCodec; import fr.uca.iut.codecs.GenericCodec;
import fr.uca.iut.entities.Trainer; 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 fr.uca.iut.utils.enums.PokemongName;
import org.bson.BsonReader; import org.bson.BsonReader;
import org.bson.BsonWriter; import org.bson.BsonWriter;
@ -12,11 +12,13 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext; import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext; import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class TrainerCodec extends GenericCodec<Trainer> { public class TrainerCodec extends GenericCodec<Trainer> {
@ -24,21 +26,66 @@ public class TrainerCodec extends GenericCodec<Trainer> {
public TrainerCodec() { public TrainerCodec() {
this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() 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<String> pastOpponentsIds = document.getList("pastOpponents", ObjectId.class)
.stream()
.map(ObjectId::toString)
.collect(Collectors.toList());
trainer.setPastOpponents(pastOpponentsIds);
Set<TrainerPokemong> 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 @Override
public void encode(BsonWriter writer, Trainer trainer, EncoderContext encoderContext) { public void encode(BsonWriter writer, @NotNull Trainer trainer, EncoderContext encoderContext) {
Document doc = new Document(); Document doc = new Document();
doc.put("_id", new ObjectId(trainer.getId())); doc.put("_id", new ObjectId(trainer.getId()));
doc.put("schemaVersion", trainer.getSchemaVersion());
doc.put("name", trainer.getName()); doc.put("name", trainer.getName());
LocalDate dob = trainer.getDob(); LocalDate dob = trainer.getDob();
if (dob != null) { if (dob != null) {
doc.put("dob", Date.from(dob.atStartOfDay(ZoneId.systemDefault()) doc.put("dob", Date.from(dob.atStartOfDay(ZoneId.systemDefault())
.toInstant())); .toInstant()));
} }
doc.put("wins", trainer.getWins()); doc.put("wins", trainer.getWins());
@ -46,23 +93,23 @@ public class TrainerCodec extends GenericCodec<Trainer> {
doc.put("losses", trainer.getLosses()); doc.put("losses", trainer.getLosses());
List<ObjectId> pastOpponentsIds = trainer.getPastOpponents() List<ObjectId> pastOpponentsIds = trainer.getPastOpponents()
.stream() .stream()
.map(ObjectId::new) .map(ObjectId::new)
.collect(Collectors.toList()); .collect(Collectors.toList());
doc.put("pastOpponents", pastOpponentsIds); doc.put("pastOpponents", pastOpponentsIds);
List<Document> pokemongListDoc = trainer.getPokemongs() List<Document> pokemongListDoc = trainer.getPokemongs()
.stream() .stream()
.map(pokemong -> { .map(pokemong -> {
Document moveDoc = new Document(); Document moveDoc = new Document();
moveDoc.put("_id", new ObjectId(pokemong.getId())); moveDoc.put("_id", new ObjectId(pokemong.getId()));
moveDoc.put("nickname", pokemong.getNickname()); moveDoc.put("nickname", pokemong.getNickname());
moveDoc.put("species", moveDoc.put("species",
pokemong.getSpecies() pokemong.getSpecies()
.name()); .name());
return moveDoc; return moveDoc;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
doc.put("pokemongs", pokemongListDoc); doc.put("pokemongs", pokemongListDoc);
documentCodec.encode(writer, doc, encoderContext); documentCodec.encode(writer, doc, encoderContext);
@ -76,42 +123,12 @@ public class TrainerCodec extends GenericCodec<Trainer> {
@Override @Override
public Trainer decode(BsonReader reader, DecoderContext decoderContext) { public Trainer decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, 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<String> pastOpponentsIds = document.getList("pastOpponents", ObjectId.class) Integer schemaVersion = document.getInteger("schemaVersion");
.stream()
.map(ObjectId::toString)
.collect(Collectors.toList());
trainer.setPastOpponents(pastOpponentsIds);
List<TrainerPokemong> pokemongList = document return switch (schemaVersion) {
.getList("pokemongs", Document.class) case 1 -> decodeV1(document);
.stream() default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
.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;
} }
} }

@ -5,11 +5,12 @@ import fr.uca.iut.entities.Trainer;
import org.bson.codecs.Codec; import org.bson.codecs.Codec;
import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.configuration.CodecRegistry;
import org.jetbrains.annotations.NotNull;
public class TrainerCodecProvider implements CodecProvider { public class TrainerCodecProvider implements CodecProvider {
@Nullable @Nullable
@Override @Override
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) { public <T> Codec<T> get(@NotNull Class<T> clazz, CodecRegistry registry) {
if (clazz.equals(Trainer.class)) { if (clazz.equals(Trainer.class)) {
return (Codec<T>) new TrainerCodec(); return (Codec<T>) new TrainerCodec();
} }

@ -1,6 +1,6 @@
package fr.uca.iut.codecs.type; 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 fr.uca.iut.utils.enums.TypeName;
import org.bson.Document; import org.bson.Document;
@ -12,15 +12,15 @@ public class TypeCodecUtil {
Type type = new Type(); Type type = new Type();
type.setName(TypeName.valueOf(typeDoc.getString("name"))); type.setName(TypeName.valueOf(typeDoc.getString("name")));
List<TypeName> weakAgainst = typeDoc.getList("weakAgainst", String.class) List<TypeName> weakAgainst = typeDoc.getList("weakAgainst", String.class)
.stream() .stream()
.map(TypeName::valueOf) .map(TypeName::valueOf)
.collect(Collectors.toList()); .collect(Collectors.toList());
type.setWeakAgainst(weakAgainst); type.setWeakAgainst(weakAgainst);
List<TypeName> effectiveAgainst = typeDoc.getList("effectiveAgainst", List<TypeName> effectiveAgainst = typeDoc.getList("effectiveAgainst",
String.class) String.class)
.stream() .stream()
.map(TypeName::valueOf) .map(TypeName::valueOf)
.collect(Collectors.toList()); .collect(Collectors.toList());
type.setEffectiveAgainst(effectiveAgainst); type.setEffectiveAgainst(effectiveAgainst);
return type; return type;
} }

@ -22,24 +22,23 @@ public abstract class GenericController<T extends GenericEntity> {
T entity = service.getOneById(id); T entity = service.getOneById(id);
if (entity != null) { if (entity != null) {
return Response.ok(entity) return Response.ok(entity)
.build(); .build();
} } else {
else {
return Response.status(Response.Status.NOT_FOUND) return Response.status(Response.Status.NOT_FOUND)
.entity("Entity not found for id: " + id) .entity("Entity not found for id: " + id)
.build(); .build();
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST) return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid id format: " + id) .entity(e.getMessage())
.build(); .build();
} }
} }
@GET @GET
public Response getAll() { public Response getAll() {
return Response.ok(service.getAll()) return Response.ok(service.getAll())
.build(); .build();
} }
@POST @POST
@ -47,17 +46,16 @@ public abstract class GenericController<T extends GenericEntity> {
public Response createOne(T entity) { public Response createOne(T entity) {
try { try {
service.validateOne(entity);
T newEntity = service.addOne(entity); T newEntity = service.addOne(entity);
return Response.status(Response.Status.CREATED) return Response.status(Response.Status.CREATED)
.entity(newEntity) .entity(newEntity)
.build(); .build();
} catch (NonValidEntityException e) { } catch (NonValidEntityException e) {
return Response.status(Response.Status.BAD_REQUEST) return Response.status(Response.Status.BAD_REQUEST)
.entity(e.getMessage()) .entity(e.getMessage())
.build(); .build();
} }
} }
@ -66,28 +64,22 @@ public abstract class GenericController<T extends GenericEntity> {
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
public Response updateOne(@PathParam("id") String id, T entity) { public Response updateOne(@PathParam("id") String id, T entity) {
try { try {
service.validateOne(entity);
entity.setId(id); entity.setId(id);
T updatedEntity = service.updateOne(entity); T updatedEntity = service.updateOne(entity);
if (updatedEntity != null) { if (updatedEntity != null) {
return Response.status(Response.Status.OK) return Response.status(Response.Status.OK)
.entity(updatedEntity) .entity(updatedEntity)
.build(); .build();
} } else {
else {
return Response.status(Response.Status.NOT_FOUND) return Response.status(Response.Status.NOT_FOUND)
.entity("Entity not found for id: " + id) .entity("Entity not found for id: " + id)
.build(); .build();
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException | NonValidEntityException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid id format: " + id)
.build();
} catch (NonValidEntityException e) {
return Response.status(Response.Status.BAD_REQUEST) return Response.status(Response.Status.BAD_REQUEST)
.entity(e.getMessage()) .entity(e.getMessage())
.build(); .build();
} }
} }
@ -97,12 +89,12 @@ public abstract class GenericController<T extends GenericEntity> {
try { try {
service.deleteOneById(id); service.deleteOneById(id);
return Response.ok() return Response.ok()
.build(); .build();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST) return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid id format: " + id) .entity(e.getMessage())
.build(); .build();
} }
} }
} }

@ -0,0 +1,21 @@
package fr.uca.iut.entities;
/**
* The strategy for incrementing the schema version number is simple.
* <br><br>
* `schemaVersion` will have to start at 1, and need to be incremented by one at each schema change.
* <br><br>
* 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;
}
}

@ -1,9 +1,11 @@
package fr.uca.iut.entities; package fr.uca.iut.entities;
import fr.uca.iut.entities.embedded.Type;
import fr.uca.iut.utils.enums.MoveCategoryName; import fr.uca.iut.utils.enums.MoveCategoryName;
public class Move extends GenericEntity { public class Move extends GenericVersionedEntity {
public static final String COLLECTION_NAME = "moves"; public static final String COLLECTION_NAME = "moves";
public static final Integer LATEST_SCHEMA_VERSION = 2;
private String name; private String name;
private MoveCategoryName category; private MoveCategoryName category;
@ -11,7 +13,8 @@ public class Move extends GenericEntity {
private Integer accuracy; private Integer accuracy;
private Type type; private Type type;
public Move() {} public Move() {
}
public String getName() { public String getName() {
return name; return name;

@ -1,6 +1,8 @@
package fr.uca.iut.entities; package fr.uca.iut.entities;
import com.mongodb.lang.Nullable; 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 fr.uca.iut.utils.enums.PokemongName;
import java.time.LocalDate; import java.time.LocalDate;
@ -8,9 +10,11 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
public class Pokemong extends GenericEntity { public class Pokemong extends GenericVersionedEntity {
public static final String COLLECTION_NAME = "pokemongs"; public static final String COLLECTION_NAME = "pokemongs";
public static final Integer LATEST_SCHEMA_VERSION = 1;
@Nullable @Nullable
private String nickname; private String nickname;
private LocalDate dob; private LocalDate dob;
@ -20,14 +24,15 @@ public class Pokemong extends GenericEntity {
private List<PokemongName> evoTrack; private List<PokemongName> evoTrack;
@Nullable @Nullable
private String trainer; private String trainer;
private List<Type> types; private Set<Type> types;
/** /**
* pokemong.moveSet: [{_id: ObjectId, name: String}] * pokemong.moveSet: [{_id: ObjectId, name: String}]
*/ */
private Set<PokemongMove> moveSet; private Set<PokemongMove> moveSet;
public Pokemong() {} public Pokemong() {
}
@Nullable @Nullable
public String getNickname() { public String getNickname() {
@ -71,11 +76,11 @@ public class Pokemong extends GenericEntity {
this.trainer = trainer; this.trainer = trainer;
} }
public List<Type> getTypes() { public Set<Type> getTypes() {
return Collections.unmodifiableList(types); return Collections.unmodifiableSet(types);
} }
public void setTypes(List<Type> types) { public void setTypes(Set<Type> types) {
this.types = types; this.types = types;
} }
@ -96,8 +101,7 @@ public class Pokemong extends GenericEntity {
public void updateMove(String id, String name) { public void updateMove(String id, String name) {
for (PokemongMove move : moveSet) { for (PokemongMove move : moveSet) {
if (move.getId() if (move.getId()
.equals(id)) .equals(id)) {
{
move.setName(name); move.setName(name);
break; break;
} }
@ -109,7 +113,11 @@ public class Pokemong extends GenericEntity {
} }
public List<PokemongName> getEvoTrack() { public List<PokemongName> getEvoTrack() {
return evoTrack; return Collections.unmodifiableList(evoTrack);
}
public void setEvoTrack(List<PokemongName> evoTrack) {
this.evoTrack = evoTrack;
} }
public Integer getEvoStage() { public Integer getEvoStage() {
@ -119,9 +127,5 @@ public class Pokemong extends GenericEntity {
public void setEvoStage(Integer evoStage) { public void setEvoStage(Integer evoStage) {
this.evoStage = evoStage; this.evoStage = evoStage;
} }
public void setEvoTrack(List<PokemongName> evoTrack) {
this.evoTrack = evoTrack;
}
} }

@ -1,20 +1,25 @@
package fr.uca.iut.entities; package fr.uca.iut.entities;
import fr.uca.iut.entities.denormalized.TrainerPokemong;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 String COLLECTION_NAME = "trainers";
public static final Integer LATEST_SCHEMA_VERSION = 1;
private String name; private String name;
private LocalDate dob; private LocalDate dob;
private Integer wins; private Integer wins;
private Integer losses; private Integer losses;
private List<String> pastOpponents; private List<String> pastOpponents;
private List<TrainerPokemong> pokemongs; private Set<TrainerPokemong> pokemongs;
public Trainer() {} public Trainer() {
}
public String getName() { public String getName() {
return name; return name;
@ -56,11 +61,19 @@ public class Trainer extends GenericEntity {
this.pastOpponents = pastOpponents; this.pastOpponents = pastOpponents;
} }
public List<TrainerPokemong> getPokemongs() { public Set<TrainerPokemong> getPokemongs() {
return Collections.unmodifiableList(pokemongs); return Collections.unmodifiableSet(pokemongs);
} }
public void setPokemongs(List<TrainerPokemong> pokemongs) { public void setPokemongs(Set<TrainerPokemong> pokemongs) {
this.pokemongs = pokemongs; this.pokemongs = pokemongs;
} }
public void addPokemong(TrainerPokemong trainerPokemong) {
pokemongs.add(trainerPokemong);
}
public void removePokemong(String id) {
pokemongs.removeIf(trainerPokemong -> trainerPokemong.getId().equals(id));
}
} }

@ -1,10 +1,13 @@
package fr.uca.iut.entities; package fr.uca.iut.entities.denormalized;
import fr.uca.iut.entities.GenericEntity;
public class PokemongMove extends GenericEntity { public class PokemongMove extends GenericEntity {
private String name; private String name;
public PokemongMove() {} public PokemongMove() {
}
public String getName() { public String getName() {
return name; return name;

@ -1,6 +1,7 @@
package fr.uca.iut.entities; package fr.uca.iut.entities.denormalized;
import com.mongodb.lang.Nullable; import com.mongodb.lang.Nullable;
import fr.uca.iut.entities.GenericEntity;
import fr.uca.iut.utils.enums.PokemongName; import fr.uca.iut.utils.enums.PokemongName;
public class TrainerPokemong extends GenericEntity { public class TrainerPokemong extends GenericEntity {
@ -9,7 +10,8 @@ public class TrainerPokemong extends GenericEntity {
private PokemongName species; private PokemongName species;
public TrainerPokemong() {} public TrainerPokemong() {
}
@Nullable @Nullable
public String getNickname() { public String getNickname() {

@ -1,8 +1,7 @@
package fr.uca.iut.entities; package fr.uca.iut.entities.embedded;
import fr.uca.iut.utils.enums.TypeName; import fr.uca.iut.utils.enums.TypeName;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -12,7 +11,8 @@ public class Type {
private List<TypeName> weakAgainst; private List<TypeName> weakAgainst;
private List<TypeName> effectiveAgainst; private List<TypeName> effectiveAgainst;
public Type() {} public Type() {
}
public TypeName getName() { public TypeName getName() {
return name; return name;
@ -23,7 +23,7 @@ public class Type {
} }
public List<TypeName> getWeakAgainst() { public List<TypeName> getWeakAgainst() {
return Collections.unmodifiableList(weakAgainst); return weakAgainst;
} }
public void setWeakAgainst(List<TypeName> weakAgainst) { public void setWeakAgainst(List<TypeName> weakAgainst) {
@ -31,7 +31,7 @@ public class Type {
} }
public List<TypeName> getEffectiveAgainst() { public List<TypeName> getEffectiveAgainst() {
return Collections.unmodifiableList(effectiveAgainst); return effectiveAgainst;
} }
public void setEffectiveAgainst(List<TypeName> effectiveAgainst) { public void setEffectiveAgainst(List<TypeName> effectiveAgainst) {
@ -49,8 +49,8 @@ public class Type {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
Type type = (Type) o; Type type = (Type) o;
return Objects.equals(name, type.name) && return Objects.equals(name, type.name) &&
Objects.equals(weakAgainst, type.weakAgainst) && Objects.equals(weakAgainst, type.weakAgainst) &&
Objects.equals(effectiveAgainst, type.effectiveAgainst); Objects.equals(effectiveAgainst, type.effectiveAgainst);
} }
} }

@ -2,7 +2,9 @@ package fr.uca.iut.repositories;
import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.ReplaceOneModel;
import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.WriteModel;
import com.mongodb.lang.Nullable; import com.mongodb.lang.Nullable;
import fr.uca.iut.entities.GenericEntity; import fr.uca.iut.entities.GenericEntity;
import org.bson.Document; import org.bson.Document;
@ -27,7 +29,7 @@ public abstract class GenericRepository<T extends GenericEntity> {
@Nullable @Nullable
public T findById(String id) { public T findById(String id) {
return getCollection().find(eq("_id", new ObjectId(id))) return getCollection().find(eq("_id", new ObjectId(id)))
.first(); .first();
} }
protected abstract MongoCollection<T> getCollection(); protected abstract MongoCollection<T> getCollection();
@ -38,7 +40,7 @@ public abstract class GenericRepository<T extends GenericEntity> {
public List<T> listAll() { public List<T> listAll() {
return getCollection().find() return getCollection().find()
.into(new ArrayList<>()); .into(new ArrayList<>());
} }
public void persistOrUpdate(@NotNull T entity) { public void persistOrUpdate(@NotNull T entity) {
@ -49,6 +51,21 @@ public abstract class GenericRepository<T extends GenericEntity> {
); );
} }
public void updateAll(@NotNull List<T> entities) {
List<WriteModel<T>> 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) { public void delete(@NotNull T entity) {
getCollection().deleteOne(eq("_id", new ObjectId(entity.getId()))); getCollection().deleteOne(eq("_id", new ObjectId(entity.getId())));
} }

@ -33,7 +33,7 @@ public class PokemongRepository extends GenericRepository<Pokemong> {
public List<Pokemong> findByMove(String moveId) { public List<Pokemong> findByMove(String moveId) {
Bson filter = Filters.elemMatch("moveSet", Filters.eq("_id", new ObjectId(moveId))); Bson filter = Filters.elemMatch("moveSet", Filters.eq("_id", new ObjectId(moveId)));
return getCollection().find(filter) return getCollection().find(filter)
.into(new ArrayList<>()); .into(new ArrayList<>());
} }
@Override @Override

@ -17,10 +17,20 @@ public abstract class GenericService<T extends GenericEntity> {
} }
public T addOne(@NotNull T entity) { public T addOne(@NotNull T entity) {
validateOne(entity);
repository.persist(entity); repository.persist(entity);
return 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 @Nullable
public T getOneById(String id) { public T getOneById(String id) {
return repository.findById(id); return repository.findById(id);
@ -37,15 +47,21 @@ public abstract class GenericService<T extends GenericEntity> {
} }
} }
@Nullable
public abstract T updateOne(@NotNull T entity);
/** /**
* Override me and start with `super.validateOne(entity);` * Override me
*/ */
public void validateOne(T entity) { @Nullable
if (entity == null) { public T updateOne(@NotNull T entity) {
throw new NonValidEntityException("entity was null"); validateOne(entity);
return entity;
}
public void updateAll(List<T> entities) {
if (!entities.isEmpty()) {
for (T entity : entities) {
validateOne(entity);
}
repository.updateAll(entities);
} }
} }
} }

@ -13,6 +13,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@ApplicationScoped @ApplicationScoped
public class MoveService extends GenericService<Move> { public class MoveService extends GenericService<Move> {
@ -27,41 +29,6 @@ public class MoveService extends GenericService<Move> {
setRepository(moveRepository); setRepository(moveRepository);
} }
@Override
public void deleteOneById(String id) {
super.deleteOneById(id);
List<Pokemong> 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<Pokemong> 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 @Override
public void validateOne(Move move) { public void validateOne(Move move) {
@ -89,11 +56,87 @@ public class MoveService extends GenericService<Move> {
errors.add("move type was null or invalid"); 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()) { if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors)); throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
} }
} }
@Nullable
@Override
public Move getOneById(String id) {
return migrateToV2(super.getOneById(id));
}
@Override
public List<Move> getAll() {
return super.getAll()
.stream()
.map(this::migrateToV2)
.collect(Collectors.toList());
}
@Override
public void deleteOneById(String id) {
List<Pokemong> pokemongs = pokemongService.findByMove(id);
List<Pokemong> 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<Pokemong> pokemongs = pokemongService.findByMove(move.getId());
List<Pokemong> 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) { public boolean existsById(String moveId) {
return moveRepository.existsById(moveId); return moveRepository.existsById(moveId);
} }

@ -1,7 +1,11 @@
package fr.uca.iut.services; package fr.uca.iut.services;
import com.mongodb.lang.Nullable; 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.repositories.PokemongRepository;
import fr.uca.iut.utils.StringUtils; import fr.uca.iut.utils.StringUtils;
import fr.uca.iut.utils.enums.PokemongName; import fr.uca.iut.utils.enums.PokemongName;
@ -36,81 +40,19 @@ public class PokemongService extends GenericService<Pokemong> {
@Override @Override
public Pokemong addOne(@NotNull Pokemong pokemong) { public Pokemong addOne(@NotNull Pokemong pokemong) {
Pokemong persistedPokemong = super.addOne(pokemong); Pokemong persistedPokemong = super.addOne(pokemong);
String trainerId = pokemong.getTrainer();
Trainer trainer = trainerService.getOneById(pokemong.getTrainer()); if (trainerId != null) {
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) {
Trainer trainer = trainerService.getOneById(pokemong.getTrainer()); Trainer trainer = trainerService.getOneById(pokemong.getTrainer());
if (trainer != null) { if (trainer != null) {
trainer.getPokemongs() TrainerPokemong trainerPokemong = new TrainerPokemong();
.removeIf(trainerPokemong -> trainerPokemong.getId() trainerPokemong.setId(pokemong.getId());
.equals(id)); trainerPokemong.setNickname(pokemong.getNickname());
trainerPokemong.setSpecies(pokemong.getSpecies());
trainer.addPokemong(trainerPokemong);
trainerService.updateOne(trainer); trainerService.updateOne(trainer);
} }
} }
super.deleteOneById(id); return persistedPokemong;
}
@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;
} }
@Override @Override
@ -140,19 +82,17 @@ public class PokemongService extends GenericService<Pokemong> {
errors.add("pokemong evo track was null or invalid"); errors.add("pokemong evo track was null or invalid");
} }
List<Type> types = pokemong.getTypes(); Set<Type> types = pokemong.getTypes();
if (types == null if (types == null
|| types.size() == 0 || types.size() == 0
|| types.size() > 2) || types.size() > 2) {
{
errors.add("pokemong types was null or empty or had more than 2 types"); errors.add("pokemong types was null or empty or had more than 2 types");
} }
Set<PokemongMove> moveSet = pokemong.getMoveSet(); Set<PokemongMove> moveSet = pokemong.getMoveSet();
if (moveSet == null) { if (moveSet == null) {
errors.add("pokemong move set was null"); errors.add("pokemong move set was null");
} } else {
else {
if (moveSet.size() == 0 || moveSet.size() > 4) { if (moveSet.size() == 0 || moveSet.size() > 4) {
errors.add("pokemong move set was empty or had more than 4 moves"); errors.add("pokemong move set was empty or had more than 4 moves");
} }
@ -171,11 +111,88 @@ public class PokemongService extends GenericService<Pokemong> {
} }
} }
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()) { if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors)); 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<Pokemong> findByMove(String id) { public List<Pokemong> findByMove(String id) {
return pokemongRepository.findByMove(id); return pokemongRepository.findByMove(id);
} }
@ -190,13 +207,16 @@ public class PokemongService extends GenericService<Pokemong> {
return repository.existsById(pokemongId); return repository.existsById(pokemongId);
} }
public void batchUpdatePokemongTrainers(List<TrainerPokemong> trainerPokemongs, @Nullable String trainerId) { public void batchUpdatePokemongTrainers(@NotNull Set<TrainerPokemong> trainerPokemongs,
@Nullable String trainerId) {
List<Pokemong> pokemongsToUpdate = new ArrayList<>();
for (TrainerPokemong trainerPokemong : trainerPokemongs) { for (TrainerPokemong trainerPokemong : trainerPokemongs) {
Pokemong pokemong = getOneById(trainerPokemong.getId()); Pokemong pokemong = getOneById(trainerPokemong.getId());
if (pokemong != null) { if (pokemong != null && !Objects.equals(pokemong.getTrainer(), trainerId)) {
pokemong.setTrainer(trainerId); pokemong.setTrainer(trainerId);
updateOne(pokemong); pokemongsToUpdate.add(pokemong);
} }
} }
updateAll(pokemongsToUpdate);
} }
} }

@ -3,7 +3,7 @@ package fr.uca.iut.services;
import com.mongodb.lang.Nullable; import com.mongodb.lang.Nullable;
import fr.uca.iut.entities.Pokemong; import fr.uca.iut.entities.Pokemong;
import fr.uca.iut.entities.Trainer; 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.repositories.TrainerRepository;
import fr.uca.iut.utils.StringUtils; import fr.uca.iut.utils.StringUtils;
import fr.uca.iut.utils.exceptions.NonValidEntityException; import fr.uca.iut.utils.exceptions.NonValidEntityException;
@ -12,8 +12,8 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.stream.Collectors;
@ApplicationScoped @ApplicationScoped
public class TrainerService extends GenericService<Trainer> { public class TrainerService extends GenericService<Trainer> {
@ -33,38 +33,14 @@ public class TrainerService extends GenericService<Trainer> {
public Trainer addOne(@NotNull Trainer trainer) { public Trainer addOne(@NotNull Trainer trainer) {
Trainer persistedTrainer = super.addOne(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()); pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), trainer.getId());
return persistedTrainer; 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 @Override
public void validateOne(Trainer trainer) { public void validateOne(Trainer trainer) {
@ -92,45 +68,118 @@ public class TrainerService extends GenericService<Trainer> {
if (pastOpponents == null) { if (pastOpponents == null) {
errors.add("trainer past opponents collection was null"); errors.add("trainer past opponents collection was null");
} } else {
else {
for (String trainerId : pastOpponents) { for (String trainerId : pastOpponents) {
if (StringUtils.isBlankStringOrNull(trainerId) || !trainerRepository.existsById(trainerId)) { if (StringUtils.isBlankStringOrNull(trainerId)) {
errors.add("trainer past opponents collection contained an invalid or unknown id"); errors.add("trainer past opponents collection contained an invalid id: " + trainerId);
} }
} }
} }
List<TrainerPokemong> pokemongs = trainer.getPokemongs(); Set<TrainerPokemong> pokemongs = trainer.getPokemongs();
if (pokemongs == null) { if (pokemongs == null) {
errors.add("trainer pokemongs collection was null or invalid"); errors.add("trainer pokemongs collection was null or invalid");
} } else {
else {
for (TrainerPokemong pokemong : pokemongs) { for (TrainerPokemong pokemong : pokemongs) {
String pokemongId = pokemong.getId(); String pokemongId = pokemong.getId();
if (StringUtils.isBlankStringOrNull(pokemongId) || !pokemongService.existsById(pokemongId)) { if (StringUtils.isBlankStringOrNull(pokemongId) || !pokemongService.existsById(pokemongId)) {
errors.add("pokemong with id " + pokemongId + " does not exist"); errors.add("pokemong with id " + pokemongId + " does not exist");
} } else {
else {
if (!pokemongService.isEvoValid(pokemongId, pokemong.getSpecies())) { if (!pokemongService.isEvoValid(pokemongId, pokemong.getSpecies())) {
errors.add("pokemong with id " + pokemongId + " cannot be a " + errors.add("pokemong with id " + pokemongId + " cannot be a " +
pokemong.getSpecies()); pokemong.getSpecies());
} }
Pokemong pokemongBehind = pokemongService.getOneById(pokemongId); Pokemong pokemongBehind = pokemongService.getOneById(pokemongId);
if (pokemong.getNickname() != null if (pokemong.getNickname() != null
&& pokemongBehind != null && pokemongBehind != null
&& !pokemong.getNickname() && !pokemong.getNickname()
.equals(pokemongBehind.getNickname())) .equals(pokemongBehind.getNickname())) {
{
errors.add("pokemong with id " + pokemongId + " already has a nickname"); 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()) { if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors)); 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<TrainerPokemong> 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<TrainerPokemong> 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<TrainerPokemong> oldPokemongs,
@NotNull Set<TrainerPokemong> newPokemongs
) {
List<Trainer> 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);
}
} }

@ -310,6 +310,7 @@ components:
- evoTrack - evoTrack
- types - types
- moveSet - moveSet
- schemaVersion
properties: properties:
nickname: nickname:
type: string type: string
@ -347,6 +348,8 @@ components:
maxItems: 4 maxItems: 4
items: items:
$ref: '#/components/schemas/PokemongMove' $ref: '#/components/schemas/PokemongMove'
schemaVersion:
$ref: '#/components/schemas/SchemaVersion'
Move: Move:
type: object type: object
@ -356,6 +359,7 @@ components:
- category - category
- accuracy - accuracy
- type - type
- schemaVersion
properties: properties:
name: name:
type: string type: string
@ -370,6 +374,8 @@ components:
minimum: 0 minimum: 0
type: type:
$ref: '#/components/schemas/TypeName' $ref: '#/components/schemas/TypeName'
schemaVersion:
$ref: '#/components/schemas/SchemaVersion'
Trainer: Trainer:
type: object type: object
@ -380,6 +386,7 @@ components:
- losses - losses
- pastOpponents - pastOpponents
- pokemongs - pokemongs
- schemaVersion
properties: properties:
name: name:
type: string type: string
@ -402,6 +409,8 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/TrainerPokemong' $ref: '#/components/schemas/TrainerPokemong'
schemaVersion:
$ref: '#/components/schemas/SchemaVersion'
PokemongMove: PokemongMove:
type: object type: object
@ -431,6 +440,11 @@ components:
species: species:
$ref: '#/components/schemas/PokemongName' $ref: '#/components/schemas/PokemongName'
SchemaVersion:
type: integer
minimum: 1
description: must be >= 1, and furthermore must be >= latest schema version
MoveCategoryName: MoveCategoryName:
type: string type: string
enum: [ enum: [

Loading…
Cancel
Save