📍 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
pull/10/head
Alexis Drai 2 years ago committed by alexis.drai@etu.uca.fr
parent 6565b12586
commit e1088d8af3

1
.gitignore vendored

@ -85,3 +85,4 @@ gradle-app.setting
# Others
docs/todos.md
/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
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
<img src="./docs/mcd.png" alt="Data Concept Model" title="Data Concept Model">
### 🧬 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
### ♨️ 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://<username>:<password>@<cluster>.<node>.mongodb.net/<databasename> --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

@ -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.EncoderContext;
import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
public abstract class GenericCodec<T extends GenericEntity> implements CollectibleCodec<T> {
@ -27,12 +28,12 @@ public abstract class GenericCodec<T extends GenericEntity> 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()));
}

@ -4,7 +4,7 @@ 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.entities.embedded.Type;
import fr.uca.iut.utils.enums.MoveCategoryName;
import org.bson.BsonReader;
import org.bson.BsonWriter;
@ -13,6 +13,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;
public class MoveCodec extends GenericCodec<Move> {
private final Codec<Document> documentCodec;
@ -23,11 +24,13 @@ public class MoveCodec extends GenericCodec<Move> {
}
@Override
public void encode(BsonWriter writer, Move move, EncoderContext encoderContext) {
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());
@ -55,12 +58,52 @@ public class MoveCodec extends GenericCodec<Move> {
@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")));
@ -75,4 +118,5 @@ public class MoveCodec extends GenericCodec<Move> {
return move;
}
}

@ -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;
@ -30,11 +31,13 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
}
@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",
@ -102,11 +105,23 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
@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());
pokemong.setSchemaVersion(document.getInteger("schemaVersion"));
pokemong.setNickname(document.getString("nickname"));
Date dob = document.getDate("dob");
@ -133,10 +148,10 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
pokemong.setTrainer(trainerId.toString());
}
List<Type> types = document.getList("types", Document.class)
Set<Type> types = document.getList("types", Document.class)
.stream()
.map(TypeCodecUtil::extractType)
.collect(Collectors.toList());
.collect(Collectors.toSet());
pokemong.setTypes(types);
Set<PokemongMove> moveSet = document.getList("moveSet", Document.class)

@ -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 <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)) {
return (Codec<T>) new PokemongCodec();
}

@ -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<Trainer> {
@ -27,12 +29,57 @@ public class TrainerCodec extends GenericCodec<Trainer> {
.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
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();
@ -76,42 +123,12 @@ public class TrainerCodec extends GenericCodec<Trainer> {
@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<String> pastOpponentsIds = document.getList("pastOpponents", ObjectId.class)
.stream()
.map(ObjectId::toString)
.collect(Collectors.toList());
trainer.setPastOpponents(pastOpponentsIds);
Integer schemaVersion = document.getInteger("schemaVersion");
List<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.toList());
trainer.setPokemongs(pokemongList);
return trainer;
return switch (schemaVersion) {
case 1 -> decodeV1(document);
default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
};
}
}

@ -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 <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)) {
return (Codec<T>) new TrainerCodec();
}

@ -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;

@ -23,15 +23,14 @@ public abstract class GenericController<T extends GenericEntity> {
if (entity != null) {
return Response.ok(entity)
.build();
}
else {
} else {
return Response.status(Response.Status.NOT_FOUND)
.entity("Entity not found for id: " + id)
.build();
}
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid id format: " + id)
.entity(e.getMessage())
.build();
}
}
@ -47,7 +46,6 @@ public abstract class GenericController<T extends GenericEntity> {
public Response createOne(T entity) {
try {
service.validateOne(entity);
T newEntity = service.addOne(entity);
return Response.status(Response.Status.CREATED)
@ -66,7 +64,6 @@ public abstract class GenericController<T extends GenericEntity> {
@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);
@ -74,17 +71,12 @@ public abstract class GenericController<T extends GenericEntity> {
return Response.status(Response.Status.OK)
.entity(updatedEntity)
.build();
}
else {
} else {
return Response.status(Response.Status.NOT_FOUND)
.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();
@ -101,7 +93,7 @@ public abstract class GenericController<T extends GenericEntity> {
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid id format: " + id)
.entity(e.getMessage())
.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;
import fr.uca.iut.entities.embedded.Type;
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 Integer LATEST_SCHEMA_VERSION = 2;
private String name;
private MoveCategoryName category;
@ -11,7 +13,8 @@ public class Move extends GenericEntity {
private Integer accuracy;
private Type type;
public Move() {}
public Move() {
}
public String getName() {
return name;

@ -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<PokemongName> evoTrack;
@Nullable
private String trainer;
private List<Type> types;
private Set<Type> types;
/**
* pokemong.moveSet: [{_id: ObjectId, name: String}]
*/
private Set<PokemongMove> moveSet;
public Pokemong() {}
public Pokemong() {
}
@Nullable
public String getNickname() {
@ -71,11 +76,11 @@ public class Pokemong extends GenericEntity {
this.trainer = trainer;
}
public List<Type> getTypes() {
return Collections.unmodifiableList(types);
public Set<Type> getTypes() {
return Collections.unmodifiableSet(types);
}
public void setTypes(List<Type> types) {
public void setTypes(Set<Type> 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<PokemongName> getEvoTrack() {
return evoTrack;
return Collections.unmodifiableList(evoTrack);
}
public void setEvoTrack(List<PokemongName> 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<PokemongName> evoTrack) {
this.evoTrack = evoTrack;
}
}

@ -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<String> pastOpponents;
private List<TrainerPokemong> pokemongs;
private Set<TrainerPokemong> pokemongs;
public Trainer() {}
public Trainer() {
}
public String getName() {
return name;
@ -56,11 +61,19 @@ public class Trainer extends GenericEntity {
this.pastOpponents = pastOpponents;
}
public List<TrainerPokemong> getPokemongs() {
return Collections.unmodifiableList(pokemongs);
public Set<TrainerPokemong> getPokemongs() {
return Collections.unmodifiableSet(pokemongs);
}
public void setPokemongs(List<TrainerPokemong> pokemongs) {
public void setPokemongs(Set<TrainerPokemong> 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 {
private String name;
public PokemongMove() {}
public PokemongMove() {
}
public String getName() {
return name;

@ -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() {

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

@ -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;
@ -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) {
getCollection().deleteOne(eq("_id", new ObjectId(entity.getId())));
}

@ -17,10 +17,20 @@ public abstract class GenericService<T extends GenericEntity> {
}
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<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) {
if (entity == null) {
throw new NonValidEntityException("entity was null");
@Nullable
public T updateOne(@NotNull T entity) {
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.List;
import java.util.Objects;
import java.util.stream.Collectors;
@ApplicationScoped
public class MoveService extends GenericService<Move> {
@ -27,41 +29,6 @@ public class MoveService extends GenericService<Move> {
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
public void validateOne(Move move) {
@ -89,11 +56,87 @@ public class MoveService extends GenericService<Move> {
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<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) {
return moveRepository.existsById(moveId);
}

@ -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<Pokemong> {
@Override
public Pokemong addOne(@NotNull Pokemong pokemong) {
Pokemong persistedPokemong = super.addOne(pokemong);
String trainerId = pokemong.getTrainer();
if (trainerId != null) {
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) {
Trainer trainer = trainerService.getOneById(pokemong.getTrainer());
if (trainer != null) {
trainer.getPokemongs()
.removeIf(trainerPokemong -> trainerPokemong.getId()
.equals(id));
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());
}
trainer.addPokemong(trainerPokemong);
trainerService.updateOne(trainer);
}
}
}
}
return existingPokemong;
return persistedPokemong;
}
@Override
@ -140,19 +82,17 @@ public class PokemongService extends GenericService<Pokemong> {
errors.add("pokemong evo track was null or invalid");
}
List<Type> types = pokemong.getTypes();
Set<Type> types = pokemong.getTypes();
if (types == null
|| types.size() == 0
|| types.size() > 2)
{
|| types.size() > 2) {
errors.add("pokemong types was null or empty or had more than 2 types");
}
Set<PokemongMove> 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<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()) {
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) {
return pokemongRepository.findByMove(id);
}
@ -190,13 +207,16 @@ public class PokemongService extends GenericService<Pokemong> {
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) {
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);
}
}

@ -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<Trainer> {
@ -33,38 +33,14 @@ public class TrainerService extends GenericService<Trainer> {
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,27 +68,24 @@ public class TrainerService extends GenericService<Trainer> {
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<TrainerPokemong> pokemongs = trainer.getPokemongs();
Set<TrainerPokemong> 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());
@ -121,16 +94,92 @@ public class TrainerService extends GenericService<Trainer> {
if (pokemong.getNickname() != null
&& pokemongBehind != null
&& !pokemong.getNickname()
.equals(pokemongBehind.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<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
- 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: [

Loading…
Cancel
Save