diff --git a/.gitignore b/.gitignore index 0c6da4a..019b6de 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ gradle-app.setting # Others docs/todos.md /src/test/resources/application.properties +/docs/sample-dataset/load_data.sh diff --git a/README.md b/README.md index e82d5e6..c5eb299 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,71 @@ Instructions are [here](https://clientserveur-courses.clubinfo-clermont.fr/Notat ## About -A "Pokemong" is a playful term for a `MongoDB` pocket monster. +A "Pokemong" is a playful term for a MongoDB pocket monster. -The application is developed using the Quarkus framework and uses `MongoDB` as its database. +The application is developed using the Quarkus framework and uses MongoDB as its database. -This application is a RESTful service designed to emulate a basic Pokemong management system. It allows users to perform -CRUD operations on Pokemongs, trainers, moves, and types. +This application is a RESTful service designed to emulate a basic `Pokemong` management system. It allows users to +perform +CRUD operations on `Pokemongs`, `Trainers`, `Moves`, and `Types`. ### 🗂️ DCM + Data Concept Model ### 🧬 UML Class diagram + UML Class Diagram +### NoSQL Schema Versioning Strategy + +This application uses MongoDB, a NoSQL database, which provides flexibility in our data model. While this flexibility +has +its advantages, it poses a unique challenge when we need to update our data model, specifically when we want to +introduce breaking changes in the existing schema. + +We have adopted a schema versioning strategy to overcome this challenge and manage these changes efficiently. + +#### Schema Versioning Pattern + +Schema versioning is a pattern that involves tagging each document in a collection with a version number. This version +number corresponds to the schema of the document and is used to handle schema changes in the code that reads these +documents. + +Each entity in our model extends a `GenericVersionedEntity` class, which includes a `schemaVersion` field. This field is +an integer that starts at 1 and is to be incremented by one with each schema change. Every change to the schema needs to +involve the schema version number being incremented. + +#### Incremental Document Migration + +When a document is read from the database, the version number in the document is checked. If the version number is less +than the current version, the document is updated to the current version, and the updated document is written back to +the database. This process effectively migrates the document to the current version. + +In the example of the `Move` class, the codec's `decodeV1` method handles documents with a `schemaVersion` of less +than `2`. When it reads a document with this version, it updates the `schemaVersion` to `2`, and writes the updated +document back to the database. + +```java +Move decodeV1(Document document){ + // ... + // Increment the schemaVersion to the current version + move.setSchemaVersion(2); + + // Save the updated Move object back to the database + moveRepository.persistOrUpdate(move); + // ... + } +``` + +This strategy allows for graceful schema evolution in a NoSQL environment. Instead of requiring all documents to be +migrated at once, which can be a time-consuming operation for large collections, it enables incremental document +migration. This approach also helps to avoid downtime during schema migration, as the application continues to function +correctly regardless of the document version. As documents are read, they are updated to the current schema version, +allowing the schema migration to happen gradually over time. + +However, note that this strategy increases write operations to the database, which could affect application performance. + ## Prep steps ### ♨️ Java version @@ -79,19 +131,30 @@ You can run the application in dev mode using: ## API testing +### 🧪 Sample dataset + +You can find a sample dataset at `docs/sample-dataset/`. Each JSON file contains a collection. + +To load the `moves` collection into an existing MongoDB cluster, you may use [MongoDB Shell ("mongosh")](https://www.mongodb.com/docs/mongodb-shell/) to run +```shell script +mongoimport --uri=mongodb+srv://:@..mongodb.net/ --collection=moves --file=./docs/sample-dataset/moves.json +``` + ### 🏴‍☠️ SwaggerUI Thanks to this project's OpenAPI specs, you can explore the API in a lot of ways. A popular choice is SwaggerUI -- after you run the app, just go to http://localhost:8080/q/swagger-ui and have fun. ⚠️ Unfortunately, Swagger or Quarkus or SmallRye adds the field `id` to all request examples, but in fact ***you should -not include id** when you POST a new document.* +NOT include id** when you POST or UPDATE a new document.* The app takes care of it for you. Same thing for the field `species` with `Pokemong` documents. ### 🩺 API testing tools You can use an API testing tool such as [Postman](https://www.postman.com/) or [Insomnia](https://insomnia.rest/) to test this app. +If you use Postman, you can even import `docs/postman_collection.json`, designed to work with the `🧪 Sample dataset`. + ### 📱 Front end (later) Moving forward, the front end part of this app -- a different project -- might also come into play for trying out this diff --git a/docs/postman_collection.json b/docs/postman_collection.json new file mode 100644 index 0000000..292f8cd --- /dev/null +++ b/docs/postman_collection.json @@ -0,0 +1,384 @@ +{ + "info": { + "_postman_id": "11aa7a76-83a4-4b92-940f-528e29b66df8", + "name": "PoKeMoNg", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "25802734" + }, + "item": [ + { + "name": "move", + "item": [ + { + "name": "Create 1 move", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Bubble beam\",\r\n \"category\": \"PHYSICAL\",\r\n \"power\": 10,\r\n \"accuracy\": 85,\r\n \"type\": \r\n {\r\n \"name\": \"WATER\",\r\n \"weakAgainst\": [\"GRASS\"],\r\n \"effectiveAgainst\": [\"FIRE\", \"GROUND\"]\r\n },\r\n \"schemaVersion\": 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/move", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "move" + ] + } + }, + "response": [] + }, + { + "name": "Get all moves", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/move", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "move" + ] + } + }, + "response": [] + }, + { + "name": "Get 1 move", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "move", + "60a64f7eae945a6e60b0e917" + ] + } + }, + "response": [] + }, + { + "name": "Update 1 move", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"schemaVersion\": 2,\r\n \"name\": \"Ember UPDATED\",\r\n \"category\": \"SPECIAL\",\r\n \"power\": 40,\r\n \"accuracy\": 100,\r\n \"type\": {\r\n \"name\": \"FIRE\",\r\n \"weakAgainst\": [\r\n \"WATER\",\r\n \"GROUND\"\r\n ],\r\n \"effectiveAgainst\": [\r\n \"GRASS\"\r\n ]\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e917", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "move", + "60a64f7eae945a6e60b0e917" + ] + } + }, + "response": [] + }, + { + "name": "Delete 1 move", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/move/60a64f7eae945a6e60b0e913", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "move", + "60a64f7eae945a6e60b0e913" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "pokemong", + "item": [ + { + "name": "Create 1 pkmn", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "content-type": true + } + }, + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"nickname\": \"Blappity-bloop\",\r\n \"dob\": \"2023-05-07\",\r\n \"level\": 1,\r\n \"pokedexId\": 172,\r\n \"evoStage\": 1,\r\n \"evoTrack\": [\"PICHU\", \"PIKACHU\", \"RAICHU\"],\r\n \"types\": [\r\n {\r\n \"name\": \"ELECTRIC\",\r\n \"weakAgainst\": [\"GROUND\", \"ROCK\"],\r\n \"effectiveAgainst\": [\"WATER\", \"FLYING\"]\r\n }\r\n ],\r\n \"moveSet\": \r\n [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e917\",\r\n \"name\": \"Ember\"\r\n }\r\n ],\r\n \"schemaVersion\": 1\r\n}" + }, + "url": { + "raw": "http://localhost:8080/pokemong", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "pokemong" + ] + } + }, + "response": [] + }, + { + "name": "Get all pkmn", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/pokemong", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "pokemong" + ] + } + }, + "response": [] + }, + { + "name": "Get 1 pkmn", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "pokemong", + "60a64f7eae945a6e60b0e911" + ] + } + }, + "response": [] + }, + { + "name": "Update 1 pkmn", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"schemaVersion\": 1,\n \"nickname\": \"Sparky UPDATED\",\n \"dob\": \"1994-02-18\",\n \"level\": 15,\n \"pokedexId\": 1,\n \"evoStage\": 1,\n \"evoTrack\": [\n \"BULBASAUR\",\n \"IVYSAUR\",\n \"VENUSAUR\"\n ],\n \"trainer\": \"60a64f7eae945a6e60b0e914\",\n \"types\": [\n {\n \"name\": \"GRASS\",\n \"weakAgainst\": [\n \"FIRE\"\n ],\n \"effectiveAgainst\": [\n \"WATER\",\n \"GROUND\"\n ]\n }\n ],\n \"moveSet\": [\n {\n \"id\": \"60a64f7eae945a6e60b0e912\",\n \"name\": \"Vine Whip\"\n }\n ]\n}" + }, + "url": { + "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e911", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "pokemong", + "60a64f7eae945a6e60b0e911" + ] + } + }, + "response": [] + }, + { + "name": "Delete 1 pkmn", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/pokemong/60a64f7eae945a6e60b0e916", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "pokemong", + "60a64f7eae945a6e60b0e916" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "trainer", + "item": [ + { + "name": "Create 1 trainer", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Bloop\",\r\n \"dob\": \"1997-02-18\",\r\n \"wins\": 1,\r\n \"losses\": 50,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e915\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/trainer", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "trainer" + ] + } + }, + "response": [] + }, + { + "name": "Get all trainers", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/trainer", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "trainer" + ] + } + }, + "response": [] + }, + { + "name": "Get 1 trainer", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "trainer", + "60a64f7eae945a6e60b0e914" + ] + } + }, + "response": [] + }, + { + "name": "Update 1 trainer", + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"schemaVersion\": 1,\r\n \"name\": \"Brock\",\r\n \"dob\": \"1994-02-18\",\r\n \"wins\": 60,\r\n \"losses\": 60,\r\n \"pastOpponents\": [\r\n \"60a64f7eae945a6e60b0e914\"\r\n ],\r\n \"pokemongs\": [\r\n {\r\n \"id\": \"60a64f7eae945a6e60b0e911\",\r\n \"nickname\": \"Sparky UPDATED\",\r\n \"species\": \"IVYSAUR\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e915", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "trainer", + "60a64f7eae945a6e60b0e915" + ] + } + }, + "response": [] + }, + { + "name": "Delete 1 trainer", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/trainer/60a64f7eae945a6e60b0e914", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "trainer", + "60a64f7eae945a6e60b0e914" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/sample-dataset/moves.json b/docs/sample-dataset/moves.json new file mode 100644 index 0000000..0bfac4f --- /dev/null +++ b/docs/sample-dataset/moves.json @@ -0,0 +1,40 @@ +{ + "_id": { "$oid":"60a64f7eae945a6e60b0e912" }, + "schemaVersion": 1, + "name": "Vine Whip", + "power": 45, + "pp": 15, + "category": "PHYSICAL", + "accuracy": 100, + "type": { + "name": "GRASS", + "weakAgainst": ["FIRE"], + "effectiveAgainst": ["WATER", "GROUND"] + } +} +{ + "_id": { "$oid":"60a64f7eae945a6e60b0e913" }, + "schemaVersion": 2, + "name": "Tackle", + "power": 40, + "category": "PHYSICAL", + "accuracy": 100, + "type": { + "name": "NORMAL", + "weakAgainst": ["ROCK"], + "effectiveAgainst": [] + } +} +{ + "_id": { "$oid":"60a64f7eae945a6e60b0e917" }, + "schemaVersion": 2, + "name": "Ember", + "power": 40, + "category": "SPECIAL", + "accuracy": 100, + "type": { + "name": "FIRE", + "weakAgainst": ["WATER", "GROUND"], + "effectiveAgainst": ["GRASS"] + } +} diff --git a/docs/sample-dataset/pokemongs.json b/docs/sample-dataset/pokemongs.json new file mode 100644 index 0000000..7ba88b9 --- /dev/null +++ b/docs/sample-dataset/pokemongs.json @@ -0,0 +1,98 @@ +{ + "_id": { + "$oid": "60a64f7eae945a6e60b0e911" + }, + "schemaVersion": 1, + "nickname": "Sparky", + "dob": { + "$date": { + "$numberLong": "761597551000" + } + }, + "level": 15, + "pokedexId": 1, + "evoStage": 1, + "evoTrack": [ + "BULBASAUR", + "IVYSAUR", + "VENUSAUR" + ], + "trainer": { + "$oid": "60a64f7eae945a6e60b0e914" + }, + "types": [ + { + "name": "GRASS", + "weakAgainst": [ + "FIRE" + ], + "effectiveAgainst": [ + "WATER", + "GROUND" + ] + } + ], + "moveSet": [ + { + "_id": { + "$oid": "60a64f7eae945a6e60b0e913" + }, + "name": "Tackle" + }, + { + "_id": { + "$oid": "60a64f7eae945a6e60b0e912" + }, + "name": "Vine Whip" + } + ] +} +{ + "_id": { + "$oid": "60a64f7eae945a6e60b0e916" + }, + "schemaVersion": 1, + "nickname": "Blazey", + "dob": { + "$date": { + "$numberLong": "761597651000" + } + }, + "level": 10, + "pokedexId": 4, + "evoStage": 0, + "evoTrack": [ + "CHARMANDER", + "CHARMELEON", + "CHARIZARD" + ], + "trainer": { + "$oid": "60a64f7eae945a6e60b0e915" + }, + "types": [ + { + "name": "FIRE", + "weakAgainst": [ + "WATER", + "GROUND" + ], + "effectiveAgainst": [ + "GRASS" + ] + } + ], + "moveSet": [ + { + "_id": { + "$oid": "60a64f7eae945a6e60b0e913" + }, + "name": "Tackle" + }, + { + "_id": { + "$oid": "60a64f7eae945a6e60b0e917" + }, + "name": "Ember" + } + ] +} diff --git a/docs/sample-dataset/trainers.json b/docs/sample-dataset/trainers.json new file mode 100644 index 0000000..913b61b --- /dev/null +++ b/docs/sample-dataset/trainers.json @@ -0,0 +1,56 @@ +{ + "_id": { + "$oid": "60a64f7eae945a6e60b0e914" + }, + "schemaVersion": 1, + "name": "Ash", + "dob": { + "$date": { + "$numberLong": "761598551000" + } + }, + "wins": 100, + "losses": 50, + "pastOpponents": [ + { + "$oid": "60a64f7eae945a6e60b0e915" + } + ], + "pokemongs": [ + { + "_id": { + "$oid": "60a64f7eae945a6e60b0e911" + }, + "nickname": "Sparky", + "species": "IVYSAUR" + } + ] +} +{ + "_id": { + "$oid": "60a64f7eae945a6e60b0e915" + }, + "schemaVersion": 1, + "name": "Brock", + "dob": { + "$date": { + "$numberLong": "761596551000" + } + }, + "wins": 70, + "losses": 60, + "pastOpponents": [ + { + "$oid": "60a64f7eae945a6e60b0e914" + } + ], + "pokemongs": [ + { + "_id": { + "$oid": "60a64f7eae945a6e60b0e916" + }, + "nickname": "Blazey", + "species": "CHARMANDER" + } + ] +} diff --git a/src/main/java/fr/uca/iut/codecs/GenericCodec.java b/src/main/java/fr/uca/iut/codecs/GenericCodec.java index 25faad3..ed21526 100644 --- a/src/main/java/fr/uca/iut/codecs/GenericCodec.java +++ b/src/main/java/fr/uca/iut/codecs/GenericCodec.java @@ -9,6 +9,7 @@ import org.bson.codecs.CollectibleCodec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.types.ObjectId; +import org.jetbrains.annotations.NotNull; public abstract class GenericCodec implements CollectibleCodec { @@ -27,12 +28,12 @@ public abstract class GenericCodec implements Collectib } @Override - public boolean documentHasId(T document) { + public boolean documentHasId(@NotNull T document) { return document.getId() != null; } @Override - public BsonValue getDocumentId(T document) { + public BsonValue getDocumentId(@NotNull T document) { return new BsonObjectId(new ObjectId(document.getId())); } diff --git a/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java b/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java index 13b7ec8..deb2d6c 100644 --- a/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java +++ b/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java @@ -1,78 +1,122 @@ -package fr.uca.iut.codecs.move; - -import com.mongodb.MongoClientSettings; -import fr.uca.iut.codecs.GenericCodec; -import fr.uca.iut.codecs.type.TypeCodecUtil; -import fr.uca.iut.entities.Move; -import fr.uca.iut.entities.Type; -import fr.uca.iut.utils.enums.MoveCategoryName; -import org.bson.BsonReader; -import org.bson.BsonWriter; -import org.bson.Document; -import org.bson.codecs.Codec; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.EncoderContext; -import org.bson.types.ObjectId; - -public class MoveCodec extends GenericCodec { - private final Codec documentCodec; - - public MoveCodec() { - this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() - .get(Document.class); - } - - @Override - public void encode(BsonWriter writer, Move move, EncoderContext encoderContext) { - Document doc = new Document(); - - doc.put("_id", new ObjectId(move.getId())); - - doc.put("name", move.getName()); - - doc.put("category", move.getCategory()); - - doc.put("power", move.getPower()); - - doc.put("accuracy", move.getAccuracy()); - - Type moveType = move.getType(); - Document typeDoc = new Document(); - typeDoc.put("name", - moveType.getName() - .toString()); - typeDoc.put("weakAgainst", moveType.getWeakAgainst()); - typeDoc.put("effectiveAgainst", moveType.getEffectiveAgainst()); - doc.put("type", typeDoc); - - documentCodec.encode(writer, doc, encoderContext); - } - - @Override - public Class getEncoderClass() { - return Move.class; - } - - @Override - public Move decode(BsonReader reader, DecoderContext decoderContext) { - Document document = documentCodec.decode(reader, decoderContext); - Move move = new Move(); - - move.setId(document.getObjectId("_id") - .toString()); - - move.setName(document.getString("name")); - - move.setCategory(MoveCategoryName.valueOf(document.getString("category"))); - - move.setPower(document.getInteger("power")); - - move.setAccuracy(document.getInteger("accuracy")); - - Document typeDoc = (Document) document.get("type"); - - move.setType(TypeCodecUtil.extractType(typeDoc)); - - return move; - } -} +package fr.uca.iut.codecs.move; + +import com.mongodb.MongoClientSettings; +import fr.uca.iut.codecs.GenericCodec; +import fr.uca.iut.codecs.type.TypeCodecUtil; +import fr.uca.iut.entities.Move; +import fr.uca.iut.entities.embedded.Type; +import fr.uca.iut.utils.enums.MoveCategoryName; +import org.bson.BsonReader; +import org.bson.BsonWriter; +import org.bson.Document; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.bson.types.ObjectId; +import org.jetbrains.annotations.NotNull; + +public class MoveCodec extends GenericCodec { + private final Codec documentCodec; + + public MoveCodec() { + this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() + .get(Document.class); + } + + @Override + public void encode(BsonWriter writer, @NotNull Move move, EncoderContext encoderContext) { + Document doc = new Document(); + + doc.put("_id", new ObjectId(move.getId())); + + doc.put("schemaVersion", move.getSchemaVersion()); + + doc.put("name", move.getName()); + + doc.put("category", move.getCategory()); + + doc.put("power", move.getPower()); + + doc.put("accuracy", move.getAccuracy()); + + Type moveType = move.getType(); + Document typeDoc = new Document(); + typeDoc.put("name", + moveType.getName() + .toString()); + typeDoc.put("weakAgainst", moveType.getWeakAgainst()); + typeDoc.put("effectiveAgainst", moveType.getEffectiveAgainst()); + doc.put("type", typeDoc); + + documentCodec.encode(writer, doc, encoderContext); + } + + @Override + public Class getEncoderClass() { + return Move.class; + } + + @Override + public Move decode(BsonReader reader, DecoderContext decoderContext) { + + Document document = documentCodec.decode(reader, decoderContext); + + Integer schemaVersion = document.getInteger("schemaVersion"); + + return switch (schemaVersion) { + case 1 -> decodeV1(document); + case 2 -> decodeV2(document); + default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion); + }; + } + + private @NotNull Move decodeV1(@NotNull Document document) { + Move move = new Move(); + + move.setId(document.getObjectId("_id") + .toString()); + + move.setSchemaVersion(document.getInteger("schemaVersion")); + + move.setName(document.getString("name")); + + move.setCategory(MoveCategoryName.valueOf(document.getString("category"))); + + move.setPower(document.getInteger("power")); + + move.setAccuracy(document.getInteger("accuracy")); + + Document typeDoc = (Document) document.get("type"); + + move.setType(TypeCodecUtil.extractType(typeDoc)); + + // Read and discard the old pp field + Integer pp = document.getInteger("pp"); + + return move; + } + + private @NotNull Move decodeV2(@NotNull Document document) { + Move move = new Move(); + + move.setId(document.getObjectId("_id") + .toString()); + + move.setSchemaVersion(document.getInteger("schemaVersion")); + + move.setName(document.getString("name")); + + move.setCategory(MoveCategoryName.valueOf(document.getString("category"))); + + move.setPower(document.getInteger("power")); + + move.setAccuracy(document.getInteger("accuracy")); + + Document typeDoc = (Document) document.get("type"); + + move.setType(TypeCodecUtil.extractType(typeDoc)); + + return move; + } + +} diff --git a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java index 4d65c23..f734503 100644 --- a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java +++ b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java @@ -4,8 +4,8 @@ import com.mongodb.MongoClientSettings; import fr.uca.iut.codecs.GenericCodec; import fr.uca.iut.codecs.type.TypeCodecUtil; import fr.uca.iut.entities.Pokemong; -import fr.uca.iut.entities.PokemongMove; -import fr.uca.iut.entities.Type; +import fr.uca.iut.entities.denormalized.PokemongMove; +import fr.uca.iut.entities.embedded.Type; import fr.uca.iut.utils.enums.PokemongName; import org.bson.BsonReader; import org.bson.BsonWriter; @@ -14,6 +14,7 @@ import org.bson.codecs.Codec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.types.ObjectId; +import org.jetbrains.annotations.NotNull; import java.time.ZoneId; import java.util.Date; @@ -26,21 +27,23 @@ public class PokemongCodec extends GenericCodec { public PokemongCodec() { this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() - .get(Document.class); + .get(Document.class); } @Override - public void encode(BsonWriter writer, Pokemong pokemong, EncoderContext encoderContext) { + public void encode(BsonWriter writer, @NotNull Pokemong pokemong, EncoderContext encoderContext) { Document doc = new Document(); doc.put("_id", new ObjectId(pokemong.getId())); + doc.put("schemaVersion", pokemong.getSchemaVersion()); + doc.put("nickname", pokemong.getNickname()); doc.put("dob", Date.from(pokemong.getDob() - .atStartOfDay(ZoneId.systemDefault()) - .toInstant())); + .atStartOfDay(ZoneId.systemDefault()) + .toInstant())); doc.put("level", pokemong.getLevel()); @@ -49,9 +52,9 @@ public class PokemongCodec extends GenericCodec { doc.put("evoStage", pokemong.getEvoStage()); List evoTrack = pokemong.getEvoTrack() - .stream() - .map(Enum::name) - .collect(Collectors.toList()); + .stream() + .map(Enum::name) + .collect(Collectors.toList()); doc.put("evoTrack", evoTrack); if (pokemong.getTrainer() != null) { @@ -59,36 +62,36 @@ public class PokemongCodec extends GenericCodec { } List types = pokemong.getTypes() - .stream() - .map(type -> { - Document typeDoc = new Document(); - typeDoc.put("name", - type.getName() - .name()); - List weakAgainst = type.getWeakAgainst() - .stream() - .map(Enum::name) - .collect(Collectors.toList()); - typeDoc.put("weakAgainst", weakAgainst); - List effectiveAgainst = type.getEffectiveAgainst() - .stream() - .map(Enum::name) - .collect(Collectors.toList()); - typeDoc.put("effectiveAgainst", effectiveAgainst); - return typeDoc; - }) - .collect(Collectors.toList()); + .stream() + .map(type -> { + Document typeDoc = new Document(); + typeDoc.put("name", + type.getName() + .name()); + List weakAgainst = type.getWeakAgainst() + .stream() + .map(Enum::name) + .collect(Collectors.toList()); + typeDoc.put("weakAgainst", weakAgainst); + List effectiveAgainst = type.getEffectiveAgainst() + .stream() + .map(Enum::name) + .collect(Collectors.toList()); + typeDoc.put("effectiveAgainst", effectiveAgainst); + return typeDoc; + }) + .collect(Collectors.toList()); doc.put("types", types); List moveSetDocs = pokemong.getMoveSet() - .stream() - .map(move -> { - Document moveDoc = new Document(); - moveDoc.put("_id", new ObjectId(move.getId())); - moveDoc.put("name", move.getName()); - return moveDoc; - }) - .collect(Collectors.toList()); + .stream() + .map(move -> { + Document moveDoc = new Document(); + moveDoc.put("_id", new ObjectId(move.getId())); + moveDoc.put("name", move.getName()); + return moveDoc; + }) + .collect(Collectors.toList()); doc.put("moveSet", moveSetDocs); documentCodec.encode(writer, doc, encoderContext); @@ -102,18 +105,30 @@ public class PokemongCodec extends GenericCodec { @Override public Pokemong decode(BsonReader reader, DecoderContext decoderContext) { Document document = documentCodec.decode(reader, decoderContext); + + Integer schemaVersion = document.getInteger("schemaVersion"); + + return switch (schemaVersion) { + case 1 -> decodeV1(document); + default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion); + }; + } + + private @NotNull Pokemong decodeV1(@NotNull Document document) { Pokemong pokemong = new Pokemong(); pokemong.setId(document.getObjectId("_id") - .toString()); + .toString()); + + pokemong.setSchemaVersion(document.getInteger("schemaVersion")); pokemong.setNickname(document.getString("nickname")); Date dob = document.getDate("dob"); if (dob != null) { pokemong.setDob(dob.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDate()); + .atZone(ZoneId.systemDefault()) + .toLocalDate()); } pokemong.setLevel(document.getInteger("level")); @@ -123,9 +138,9 @@ public class PokemongCodec extends GenericCodec { pokemong.setEvoStage(document.getInteger("evoStage")); List evoTrack = document.getList("evoTrack", String.class) - .stream() - .map(PokemongName::valueOf) - .collect(Collectors.toList()); + .stream() + .map(PokemongName::valueOf) + .collect(Collectors.toList()); pokemong.setEvoTrack(evoTrack); ObjectId trainerId = document.getObjectId("trainer"); @@ -133,21 +148,21 @@ public class PokemongCodec extends GenericCodec { pokemong.setTrainer(trainerId.toString()); } - List types = document.getList("types", Document.class) - .stream() - .map(TypeCodecUtil::extractType) - .collect(Collectors.toList()); + Set types = document.getList("types", Document.class) + .stream() + .map(TypeCodecUtil::extractType) + .collect(Collectors.toSet()); pokemong.setTypes(types); Set moveSet = document.getList("moveSet", Document.class) - .stream() - .map(pokemongMoveDoc -> { - PokemongMove move = new PokemongMove(); - move.setId(((ObjectId) pokemongMoveDoc.get("_id")).toString()); - move.setName(pokemongMoveDoc.getString("name")); - return move; - }) - .collect(Collectors.toSet()); + .stream() + .map(pokemongMoveDoc -> { + PokemongMove move = new PokemongMove(); + move.setId(((ObjectId) pokemongMoveDoc.get("_id")).toString()); + move.setName(pokemongMoveDoc.getString("name")); + return move; + }) + .collect(Collectors.toSet()); pokemong.setMoveSet(moveSet); return pokemong; diff --git a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java index 7327c19..fb1db0d 100644 --- a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java +++ b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodecProvider.java @@ -5,11 +5,12 @@ import fr.uca.iut.entities.Pokemong; import org.bson.codecs.Codec; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; +import org.jetbrains.annotations.NotNull; public class PokemongCodecProvider implements CodecProvider { @Nullable @Override - public Codec get(Class clazz, CodecRegistry registry) { + public Codec get(@NotNull Class clazz, CodecRegistry registry) { if (clazz.equals(Pokemong.class)) { return (Codec) new PokemongCodec(); } diff --git a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java index f9c0041..ccd874b 100644 --- a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java +++ b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java @@ -3,7 +3,7 @@ package fr.uca.iut.codecs.trainer; import com.mongodb.MongoClientSettings; import fr.uca.iut.codecs.GenericCodec; import fr.uca.iut.entities.Trainer; -import fr.uca.iut.entities.TrainerPokemong; +import fr.uca.iut.entities.denormalized.TrainerPokemong; import fr.uca.iut.utils.enums.PokemongName; import org.bson.BsonReader; import org.bson.BsonWriter; @@ -12,11 +12,13 @@ import org.bson.codecs.Codec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.types.ObjectId; +import org.jetbrains.annotations.NotNull; import java.time.LocalDate; import java.time.ZoneId; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; public class TrainerCodec extends GenericCodec { @@ -24,21 +26,66 @@ public class TrainerCodec extends GenericCodec { public TrainerCodec() { this.documentCodec = MongoClientSettings.getDefaultCodecRegistry() - .get(Document.class); + .get(Document.class); + } + + @NotNull + private static Trainer decodeV1(Document document) { + Trainer trainer = new Trainer(); + + trainer.setId(document.getObjectId("_id") + .toString()); + + trainer.setSchemaVersion(document.getInteger("schemaVersion")); + + trainer.setName(document.getString("name")); + + Date dob = document.getDate("dob"); + if (dob != null) { + trainer.setDob(dob.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate()); + } + + trainer.setWins(document.getInteger("wins")); + + trainer.setLosses(document.getInteger("losses")); + + List pastOpponentsIds = document.getList("pastOpponents", ObjectId.class) + .stream() + .map(ObjectId::toString) + .collect(Collectors.toList()); + trainer.setPastOpponents(pastOpponentsIds); + + Set pokemongList = document + .getList("pokemongs", Document.class) + .stream() + .map(pokemongDoc -> { + TrainerPokemong pokemong = new TrainerPokemong(); + pokemong.setId(((ObjectId) pokemongDoc.get("_id")).toString()); + pokemong.setNickname(pokemongDoc.getString("nickname")); + pokemong.setSpecies(PokemongName.valueOf(pokemongDoc.getString("species"))); + return pokemong; + }) + .collect(Collectors.toSet()); + trainer.setPokemongs(pokemongList); + return trainer; } @Override - public void encode(BsonWriter writer, Trainer trainer, EncoderContext encoderContext) { + public void encode(BsonWriter writer, @NotNull Trainer trainer, EncoderContext encoderContext) { Document doc = new Document(); doc.put("_id", new ObjectId(trainer.getId())); + doc.put("schemaVersion", trainer.getSchemaVersion()); + doc.put("name", trainer.getName()); LocalDate dob = trainer.getDob(); if (dob != null) { doc.put("dob", Date.from(dob.atStartOfDay(ZoneId.systemDefault()) - .toInstant())); + .toInstant())); } doc.put("wins", trainer.getWins()); @@ -46,23 +93,23 @@ public class TrainerCodec extends GenericCodec { doc.put("losses", trainer.getLosses()); List pastOpponentsIds = trainer.getPastOpponents() - .stream() - .map(ObjectId::new) - .collect(Collectors.toList()); + .stream() + .map(ObjectId::new) + .collect(Collectors.toList()); doc.put("pastOpponents", pastOpponentsIds); List pokemongListDoc = trainer.getPokemongs() - .stream() - .map(pokemong -> { - Document moveDoc = new Document(); - moveDoc.put("_id", new ObjectId(pokemong.getId())); - moveDoc.put("nickname", pokemong.getNickname()); - moveDoc.put("species", - pokemong.getSpecies() - .name()); - return moveDoc; - }) - .collect(Collectors.toList()); + .stream() + .map(pokemong -> { + Document moveDoc = new Document(); + moveDoc.put("_id", new ObjectId(pokemong.getId())); + moveDoc.put("nickname", pokemong.getNickname()); + moveDoc.put("species", + pokemong.getSpecies() + .name()); + return moveDoc; + }) + .collect(Collectors.toList()); doc.put("pokemongs", pokemongListDoc); documentCodec.encode(writer, doc, encoderContext); @@ -76,42 +123,12 @@ public class TrainerCodec extends GenericCodec { @Override public Trainer decode(BsonReader reader, DecoderContext decoderContext) { Document document = documentCodec.decode(reader, decoderContext); - Trainer trainer = new Trainer(); - - trainer.setId(document.getObjectId("_id") - .toString()); - - trainer.setName(document.getString("name")); - - Date dob = document.getDate("dob"); - if (dob != null) { - trainer.setDob(dob.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDate()); - } - - trainer.setWins(document.getInteger("wins")); - - trainer.setLosses(document.getInteger("losses")); - List pastOpponentsIds = document.getList("pastOpponents", ObjectId.class) - .stream() - .map(ObjectId::toString) - .collect(Collectors.toList()); - trainer.setPastOpponents(pastOpponentsIds); + Integer schemaVersion = document.getInteger("schemaVersion"); - List pokemongList = document - .getList("pokemongs", Document.class) - .stream() - .map(pokemongDoc -> { - TrainerPokemong pokemong = new TrainerPokemong(); - pokemong.setId(((ObjectId) pokemongDoc.get("_id")).toString()); - pokemong.setNickname(pokemongDoc.getString("nickname")); - pokemong.setSpecies(PokemongName.valueOf(pokemongDoc.getString("species"))); - return pokemong; - }) - .collect(Collectors.toList()); - trainer.setPokemongs(pokemongList); - return trainer; + return switch (schemaVersion) { + case 1 -> decodeV1(document); + default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion); + }; } } diff --git a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java index 0c3e37a..87aac26 100644 --- a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java +++ b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodecProvider.java @@ -5,11 +5,12 @@ import fr.uca.iut.entities.Trainer; import org.bson.codecs.Codec; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; +import org.jetbrains.annotations.NotNull; public class TrainerCodecProvider implements CodecProvider { @Nullable @Override - public Codec get(Class clazz, CodecRegistry registry) { + public Codec get(@NotNull Class clazz, CodecRegistry registry) { if (clazz.equals(Trainer.class)) { return (Codec) new TrainerCodec(); } diff --git a/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java b/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java index bd45965..a2d53b6 100644 --- a/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java +++ b/src/main/java/fr/uca/iut/codecs/type/TypeCodecUtil.java @@ -1,6 +1,6 @@ package fr.uca.iut.codecs.type; -import fr.uca.iut.entities.Type; +import fr.uca.iut.entities.embedded.Type; import fr.uca.iut.utils.enums.TypeName; import org.bson.Document; @@ -12,15 +12,15 @@ public class TypeCodecUtil { Type type = new Type(); type.setName(TypeName.valueOf(typeDoc.getString("name"))); List weakAgainst = typeDoc.getList("weakAgainst", String.class) - .stream() - .map(TypeName::valueOf) - .collect(Collectors.toList()); + .stream() + .map(TypeName::valueOf) + .collect(Collectors.toList()); type.setWeakAgainst(weakAgainst); List effectiveAgainst = typeDoc.getList("effectiveAgainst", - String.class) - .stream() - .map(TypeName::valueOf) - .collect(Collectors.toList()); + String.class) + .stream() + .map(TypeName::valueOf) + .collect(Collectors.toList()); type.setEffectiveAgainst(effectiveAgainst); return type; } diff --git a/src/main/java/fr/uca/iut/controllers/GenericController.java b/src/main/java/fr/uca/iut/controllers/GenericController.java index 70b30d2..6c11aeb 100644 --- a/src/main/java/fr/uca/iut/controllers/GenericController.java +++ b/src/main/java/fr/uca/iut/controllers/GenericController.java @@ -22,24 +22,23 @@ public abstract class GenericController { T entity = service.getOneById(id); if (entity != null) { return Response.ok(entity) - .build(); - } - else { + .build(); + } else { return Response.status(Response.Status.NOT_FOUND) - .entity("Entity not found for id: " + id) - .build(); + .entity("Entity not found for id: " + id) + .build(); } } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) - .entity("Invalid id format: " + id) - .build(); + .entity(e.getMessage()) + .build(); } } @GET public Response getAll() { return Response.ok(service.getAll()) - .build(); + .build(); } @POST @@ -47,17 +46,16 @@ public abstract class GenericController { public Response createOne(T entity) { try { - service.validateOne(entity); T newEntity = service.addOne(entity); return Response.status(Response.Status.CREATED) - .entity(newEntity) - .build(); + .entity(newEntity) + .build(); } catch (NonValidEntityException e) { return Response.status(Response.Status.BAD_REQUEST) - .entity(e.getMessage()) - .build(); + .entity(e.getMessage()) + .build(); } } @@ -66,28 +64,22 @@ public abstract class GenericController { @Consumes(MediaType.APPLICATION_JSON) public Response updateOne(@PathParam("id") String id, T entity) { try { - service.validateOne(entity); entity.setId(id); T updatedEntity = service.updateOne(entity); if (updatedEntity != null) { return Response.status(Response.Status.OK) - .entity(updatedEntity) - .build(); - } - else { + .entity(updatedEntity) + .build(); + } else { return Response.status(Response.Status.NOT_FOUND) - .entity("Entity not found for id: " + id) - .build(); + .entity("Entity not found for id: " + id) + .build(); } - } catch (IllegalArgumentException e) { - return Response.status(Response.Status.BAD_REQUEST) - .entity("Invalid id format: " + id) - .build(); - } catch (NonValidEntityException e) { + } catch (IllegalArgumentException | NonValidEntityException e) { return Response.status(Response.Status.BAD_REQUEST) - .entity(e.getMessage()) - .build(); + .entity(e.getMessage()) + .build(); } } @@ -97,12 +89,12 @@ public abstract class GenericController { try { service.deleteOneById(id); return Response.ok() - .build(); + .build(); } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) - .entity("Invalid id format: " + id) - .build(); + .entity(e.getMessage()) + .build(); } } } diff --git a/src/main/java/fr/uca/iut/entities/GenericVersionedEntity.java b/src/main/java/fr/uca/iut/entities/GenericVersionedEntity.java new file mode 100644 index 0000000..7ede19a --- /dev/null +++ b/src/main/java/fr/uca/iut/entities/GenericVersionedEntity.java @@ -0,0 +1,21 @@ +package fr.uca.iut.entities; + +/** + * The strategy for incrementing the schema version number is simple. + *

+ * `schemaVersion` will have to start at 1, and need to be incremented by one at each schema change. + *

+ * Every change to the schema needs to involve the schema version number being incremented. + */ +public abstract class GenericVersionedEntity extends GenericEntity { + + private Integer schemaVersion; + + public Integer getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(Integer schemaVersion) { + this.schemaVersion = schemaVersion; + } +} diff --git a/src/main/java/fr/uca/iut/entities/Move.java b/src/main/java/fr/uca/iut/entities/Move.java index da98c37..2aa1242 100644 --- a/src/main/java/fr/uca/iut/entities/Move.java +++ b/src/main/java/fr/uca/iut/entities/Move.java @@ -1,55 +1,58 @@ -package fr.uca.iut.entities; - -import fr.uca.iut.utils.enums.MoveCategoryName; - -public class Move extends GenericEntity { - public static final String COLLECTION_NAME = "moves"; - - private String name; - private MoveCategoryName category; - private Integer power; - private Integer accuracy; - private Type type; - - public Move() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public MoveCategoryName getCategory() { - return category; - } - - public void setCategory(MoveCategoryName category) { - this.category = category; - } - - public Integer getPower() { - return power; - } - - public void setPower(Integer power) { - this.power = power; - } - - public Integer getAccuracy() { - return accuracy; - } - - public void setAccuracy(Integer accuracy) { - this.accuracy = accuracy; - } - - public Type getType() { - return type; - } - - public void setType(Type type) { - this.type = type; - } +package fr.uca.iut.entities; + +import fr.uca.iut.entities.embedded.Type; +import fr.uca.iut.utils.enums.MoveCategoryName; + +public class Move extends GenericVersionedEntity { + public static final String COLLECTION_NAME = "moves"; + public static final Integer LATEST_SCHEMA_VERSION = 2; + + private String name; + private MoveCategoryName category; + private Integer power; + private Integer accuracy; + private Type type; + + public Move() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MoveCategoryName getCategory() { + return category; + } + + public void setCategory(MoveCategoryName category) { + this.category = category; + } + + public Integer getPower() { + return power; + } + + public void setPower(Integer power) { + this.power = power; + } + + public Integer getAccuracy() { + return accuracy; + } + + public void setAccuracy(Integer accuracy) { + this.accuracy = accuracy; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } } \ No newline at end of file diff --git a/src/main/java/fr/uca/iut/entities/Pokemong.java b/src/main/java/fr/uca/iut/entities/Pokemong.java index 1acd9e4..71e0248 100644 --- a/src/main/java/fr/uca/iut/entities/Pokemong.java +++ b/src/main/java/fr/uca/iut/entities/Pokemong.java @@ -1,6 +1,8 @@ package fr.uca.iut.entities; import com.mongodb.lang.Nullable; +import fr.uca.iut.entities.denormalized.PokemongMove; +import fr.uca.iut.entities.embedded.Type; import fr.uca.iut.utils.enums.PokemongName; import java.time.LocalDate; @@ -8,9 +10,11 @@ import java.util.Collections; import java.util.List; import java.util.Set; -public class Pokemong extends GenericEntity { +public class Pokemong extends GenericVersionedEntity { public static final String COLLECTION_NAME = "pokemongs"; + public static final Integer LATEST_SCHEMA_VERSION = 1; + @Nullable private String nickname; private LocalDate dob; @@ -20,14 +24,15 @@ public class Pokemong extends GenericEntity { private List evoTrack; @Nullable private String trainer; - private List types; + private Set types; /** * pokemong.moveSet: [{_id: ObjectId, name: String}] */ private Set moveSet; - public Pokemong() {} + public Pokemong() { + } @Nullable public String getNickname() { @@ -71,11 +76,11 @@ public class Pokemong extends GenericEntity { this.trainer = trainer; } - public List getTypes() { - return Collections.unmodifiableList(types); + public Set getTypes() { + return Collections.unmodifiableSet(types); } - public void setTypes(List types) { + public void setTypes(Set types) { this.types = types; } @@ -96,8 +101,7 @@ public class Pokemong extends GenericEntity { public void updateMove(String id, String name) { for (PokemongMove move : moveSet) { if (move.getId() - .equals(id)) - { + .equals(id)) { move.setName(name); break; } @@ -109,7 +113,11 @@ public class Pokemong extends GenericEntity { } public List getEvoTrack() { - return evoTrack; + return Collections.unmodifiableList(evoTrack); + } + + public void setEvoTrack(List evoTrack) { + this.evoTrack = evoTrack; } public Integer getEvoStage() { @@ -119,9 +127,5 @@ public class Pokemong extends GenericEntity { public void setEvoStage(Integer evoStage) { this.evoStage = evoStage; } - - public void setEvoTrack(List evoTrack) { - this.evoTrack = evoTrack; - } } diff --git a/src/main/java/fr/uca/iut/entities/Trainer.java b/src/main/java/fr/uca/iut/entities/Trainer.java index 6586349..a7aae20 100644 --- a/src/main/java/fr/uca/iut/entities/Trainer.java +++ b/src/main/java/fr/uca/iut/entities/Trainer.java @@ -1,20 +1,25 @@ package fr.uca.iut.entities; +import fr.uca.iut.entities.denormalized.TrainerPokemong; + import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.Set; -public class Trainer extends GenericEntity { +public class Trainer extends GenericVersionedEntity { public static final String COLLECTION_NAME = "trainers"; + public static final Integer LATEST_SCHEMA_VERSION = 1; private String name; private LocalDate dob; private Integer wins; private Integer losses; private List pastOpponents; - private List pokemongs; + private Set pokemongs; - public Trainer() {} + public Trainer() { + } public String getName() { return name; @@ -56,11 +61,19 @@ public class Trainer extends GenericEntity { this.pastOpponents = pastOpponents; } - public List getPokemongs() { - return Collections.unmodifiableList(pokemongs); + public Set getPokemongs() { + return Collections.unmodifiableSet(pokemongs); } - public void setPokemongs(List pokemongs) { + public void setPokemongs(Set pokemongs) { this.pokemongs = pokemongs; } + + public void addPokemong(TrainerPokemong trainerPokemong) { + pokemongs.add(trainerPokemong); + } + + public void removePokemong(String id) { + pokemongs.removeIf(trainerPokemong -> trainerPokemong.getId().equals(id)); + } } diff --git a/src/main/java/fr/uca/iut/entities/PokemongMove.java b/src/main/java/fr/uca/iut/entities/denormalized/PokemongMove.java similarity index 63% rename from src/main/java/fr/uca/iut/entities/PokemongMove.java rename to src/main/java/fr/uca/iut/entities/denormalized/PokemongMove.java index 13c4c31..6f0b3d8 100644 --- a/src/main/java/fr/uca/iut/entities/PokemongMove.java +++ b/src/main/java/fr/uca/iut/entities/denormalized/PokemongMove.java @@ -1,16 +1,19 @@ -package fr.uca.iut.entities; - -public class PokemongMove extends GenericEntity { - - private String name; - - public PokemongMove() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} +package fr.uca.iut.entities.denormalized; + +import fr.uca.iut.entities.GenericEntity; + +public class PokemongMove extends GenericEntity { + + private String name; + + public PokemongMove() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/fr/uca/iut/entities/TrainerPokemong.java b/src/main/java/fr/uca/iut/entities/denormalized/TrainerPokemong.java similarity index 82% rename from src/main/java/fr/uca/iut/entities/TrainerPokemong.java rename to src/main/java/fr/uca/iut/entities/denormalized/TrainerPokemong.java index f63fcb5..f13a2ee 100644 --- a/src/main/java/fr/uca/iut/entities/TrainerPokemong.java +++ b/src/main/java/fr/uca/iut/entities/denormalized/TrainerPokemong.java @@ -1,6 +1,7 @@ -package fr.uca.iut.entities; +package fr.uca.iut.entities.denormalized; import com.mongodb.lang.Nullable; +import fr.uca.iut.entities.GenericEntity; import fr.uca.iut.utils.enums.PokemongName; public class TrainerPokemong extends GenericEntity { @@ -9,7 +10,8 @@ public class TrainerPokemong extends GenericEntity { private PokemongName species; - public TrainerPokemong() {} + public TrainerPokemong() { + } @Nullable public String getNickname() { diff --git a/src/main/java/fr/uca/iut/entities/Type.java b/src/main/java/fr/uca/iut/entities/embedded/Type.java similarity index 73% rename from src/main/java/fr/uca/iut/entities/Type.java rename to src/main/java/fr/uca/iut/entities/embedded/Type.java index f9e3022..38dc8a7 100644 --- a/src/main/java/fr/uca/iut/entities/Type.java +++ b/src/main/java/fr/uca/iut/entities/embedded/Type.java @@ -1,56 +1,56 @@ -package fr.uca.iut.entities; - -import fr.uca.iut.utils.enums.TypeName; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -public class Type { - - private TypeName name; - private List weakAgainst; - private List effectiveAgainst; - - public Type() {} - - public TypeName getName() { - return name; - } - - public void setName(TypeName name) { - this.name = name; - } - - public List getWeakAgainst() { - return Collections.unmodifiableList(weakAgainst); - } - - public void setWeakAgainst(List weakAgainst) { - this.weakAgainst = weakAgainst; - } - - public List getEffectiveAgainst() { - return Collections.unmodifiableList(effectiveAgainst); - } - - public void setEffectiveAgainst(List effectiveAgainst) { - this.effectiveAgainst = effectiveAgainst; - } - - @Override - public int hashCode() { - return Objects.hash(name, weakAgainst, effectiveAgainst); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Type type = (Type) o; - return Objects.equals(name, type.name) && - Objects.equals(weakAgainst, type.weakAgainst) && - Objects.equals(effectiveAgainst, type.effectiveAgainst); - } - +package fr.uca.iut.entities.embedded; + +import fr.uca.iut.utils.enums.TypeName; + +import java.util.List; +import java.util.Objects; + +public class Type { + + private TypeName name; + private List weakAgainst; + private List effectiveAgainst; + + public Type() { + } + + public TypeName getName() { + return name; + } + + public void setName(TypeName name) { + this.name = name; + } + + public List getWeakAgainst() { + return weakAgainst; + } + + public void setWeakAgainst(List weakAgainst) { + this.weakAgainst = weakAgainst; + } + + public List getEffectiveAgainst() { + return effectiveAgainst; + } + + public void setEffectiveAgainst(List effectiveAgainst) { + this.effectiveAgainst = effectiveAgainst; + } + + @Override + public int hashCode() { + return Objects.hash(name, weakAgainst, effectiveAgainst); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Type type = (Type) o; + return Objects.equals(name, type.name) && + Objects.equals(weakAgainst, type.weakAgainst) && + Objects.equals(effectiveAgainst, type.effectiveAgainst); + } + } \ No newline at end of file diff --git a/src/main/java/fr/uca/iut/repositories/GenericRepository.java b/src/main/java/fr/uca/iut/repositories/GenericRepository.java index 649e2a4..71b208a 100644 --- a/src/main/java/fr/uca/iut/repositories/GenericRepository.java +++ b/src/main/java/fr/uca/iut/repositories/GenericRepository.java @@ -2,7 +2,9 @@ package fr.uca.iut.repositories; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.ReplaceOneModel; import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.WriteModel; import com.mongodb.lang.Nullable; import fr.uca.iut.entities.GenericEntity; import org.bson.Document; @@ -27,7 +29,7 @@ public abstract class GenericRepository { @Nullable public T findById(String id) { return getCollection().find(eq("_id", new ObjectId(id))) - .first(); + .first(); } protected abstract MongoCollection getCollection(); @@ -38,7 +40,7 @@ public abstract class GenericRepository { public List listAll() { return getCollection().find() - .into(new ArrayList<>()); + .into(new ArrayList<>()); } public void persistOrUpdate(@NotNull T entity) { @@ -49,6 +51,21 @@ public abstract class GenericRepository { ); } + public void updateAll(@NotNull List entities) { + List> updates = new ArrayList<>(); + for (T entity : entities) { + updates.add( + new ReplaceOneModel<>( + eq("_id", new ObjectId(entity.getId())), + entity, + new ReplaceOptions().upsert(true) + ) + ); + } + + getCollection().bulkWrite(updates); + } + public void delete(@NotNull T entity) { getCollection().deleteOne(eq("_id", new ObjectId(entity.getId()))); } diff --git a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java index 3da2595..6e3c634 100644 --- a/src/main/java/fr/uca/iut/repositories/PokemongRepository.java +++ b/src/main/java/fr/uca/iut/repositories/PokemongRepository.java @@ -33,7 +33,7 @@ public class PokemongRepository extends GenericRepository { public List findByMove(String moveId) { Bson filter = Filters.elemMatch("moveSet", Filters.eq("_id", new ObjectId(moveId))); return getCollection().find(filter) - .into(new ArrayList<>()); + .into(new ArrayList<>()); } @Override diff --git a/src/main/java/fr/uca/iut/services/GenericService.java b/src/main/java/fr/uca/iut/services/GenericService.java index 3bd1435..e3bdb41 100644 --- a/src/main/java/fr/uca/iut/services/GenericService.java +++ b/src/main/java/fr/uca/iut/services/GenericService.java @@ -17,10 +17,20 @@ public abstract class GenericService { } public T addOne(@NotNull T entity) { + validateOne(entity); repository.persist(entity); return entity; } + /** + * Override me and start with `super.validateOne(entity);` + */ + public void validateOne(T entity) throws NonValidEntityException { + if (entity == null) { + throw new NonValidEntityException("entity was null"); + } + } + @Nullable public T getOneById(String id) { return repository.findById(id); @@ -37,15 +47,21 @@ public abstract class GenericService { } } - @Nullable - public abstract T updateOne(@NotNull T entity); - /** - * Override me and start with `super.validateOne(entity);` + * Override me */ - public void validateOne(T entity) { - if (entity == null) { - throw new NonValidEntityException("entity was null"); + @Nullable + public T updateOne(@NotNull T entity) { + validateOne(entity); + return entity; + } + + public void updateAll(List entities) { + if (!entities.isEmpty()) { + for (T entity : entities) { + validateOne(entity); + } + repository.updateAll(entities); } } } diff --git a/src/main/java/fr/uca/iut/services/MoveService.java b/src/main/java/fr/uca/iut/services/MoveService.java index ed30c0e..590de05 100644 --- a/src/main/java/fr/uca/iut/services/MoveService.java +++ b/src/main/java/fr/uca/iut/services/MoveService.java @@ -13,6 +13,8 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; @ApplicationScoped public class MoveService extends GenericService { @@ -27,41 +29,6 @@ public class MoveService extends GenericService { setRepository(moveRepository); } - @Override - public void deleteOneById(String id) { - super.deleteOneById(id); - List pokemongs = pokemongService.findByMove(id); - for (Pokemong pokemong : pokemongs) { - pokemong.removeMove(id); - pokemongService.updateOne(pokemong); - } - } - - @Override - @Nullable - public Move updateOne(@NotNull Move move) { - Move existingMove = moveRepository.findById(move.getId()); - if (existingMove != null) { - if (!existingMove.getName() - .equals(move.getName())) - { - existingMove.setName(move.getName()); - List pokemongs = pokemongService.findByMove(move.getId()); - for (Pokemong pokemong : pokemongs) { - pokemong.updateMove(move.getId(), move.getName()); - pokemongService.updateOne(pokemong); - } - } - - existingMove.setPower(move.getPower()); - existingMove.setCategory(move.getCategory()); - existingMove.setAccuracy(move.getAccuracy()); - existingMove.setType(move.getType()); - moveRepository.persistOrUpdate(existingMove); - } - return existingMove; - } - @Override public void validateOne(Move move) { @@ -89,11 +56,87 @@ public class MoveService extends GenericService { errors.add("move type was null or invalid"); } + if (move.getSchemaVersion() == null || !Objects.equals(move.getSchemaVersion(), Move.LATEST_SCHEMA_VERSION)) { + errors.add("move schema version was null or not the latest version: " + Move.LATEST_SCHEMA_VERSION); + } + if (!errors.isEmpty()) { throw new NonValidEntityException("Validation errors: " + String.join(", ", errors)); } } + @Nullable + @Override + public Move getOneById(String id) { + return migrateToV2(super.getOneById(id)); + } + + @Override + public List getAll() { + return super.getAll() + .stream() + .map(this::migrateToV2) + .collect(Collectors.toList()); + } + + @Override + public void deleteOneById(String id) { + List pokemongs = pokemongService.findByMove(id); + List pokemongsToUpdate = new ArrayList<>(); + for (Pokemong pokemong : pokemongs) { + pokemong.removeMove(id); + pokemongsToUpdate.add(pokemong); + } + pokemongService.updateAll(pokemongsToUpdate); + super.deleteOneById(id); + } + + @Override + @Nullable + public Move updateOne(@NotNull Move move) { + super.updateOne(move); + Move existingMove = moveRepository.findById(move.getId()); + if (existingMove != null) { + if (!existingMove.getName().equals(move.getName())) { + existingMove.setName(move.getName()); + batchUpdatePokemongTrainers(move); + } + + existingMove.setPower(move.getPower()); + existingMove.setCategory(move.getCategory()); + existingMove.setAccuracy(move.getAccuracy()); + existingMove.setType(move.getType()); + moveRepository.persistOrUpdate(existingMove); + } + return existingMove; + } + + private void batchUpdatePokemongTrainers(@NotNull Move move) { + List pokemongs = pokemongService.findByMove(move.getId()); + List pokemongsToUpdate = new ArrayList<>(); + for (Pokemong pokemong : pokemongs) { + pokemong.updateMove(move.getId(), move.getName()); + pokemongsToUpdate.add(pokemong); + } + pokemongService.updateAll(pokemongsToUpdate); + } + + /** + * We want to migrate the documents incrementally, so we upgrade the + * schema version if it is less than the current schema version, + * and then save the updated document back to the database. + * + * @param move the Move found by the repository + * @return the Move(V2) based on the Move from the repository + */ + private Move migrateToV2(Move move) { + if (move != null && move.getSchemaVersion() < 2) { + move.setSchemaVersion(2); + moveRepository.persistOrUpdate(move); + } + return move; + } + public boolean existsById(String moveId) { return moveRepository.existsById(moveId); } diff --git a/src/main/java/fr/uca/iut/services/PokemongService.java b/src/main/java/fr/uca/iut/services/PokemongService.java index ae9afd9..a3c0743 100644 --- a/src/main/java/fr/uca/iut/services/PokemongService.java +++ b/src/main/java/fr/uca/iut/services/PokemongService.java @@ -1,7 +1,11 @@ package fr.uca.iut.services; import com.mongodb.lang.Nullable; -import fr.uca.iut.entities.*; +import fr.uca.iut.entities.Pokemong; +import fr.uca.iut.entities.Trainer; +import fr.uca.iut.entities.denormalized.PokemongMove; +import fr.uca.iut.entities.denormalized.TrainerPokemong; +import fr.uca.iut.entities.embedded.Type; import fr.uca.iut.repositories.PokemongRepository; import fr.uca.iut.utils.StringUtils; import fr.uca.iut.utils.enums.PokemongName; @@ -36,81 +40,19 @@ public class PokemongService extends GenericService { @Override public Pokemong addOne(@NotNull Pokemong pokemong) { Pokemong persistedPokemong = super.addOne(pokemong); - - Trainer trainer = trainerService.getOneById(pokemong.getTrainer()); - if (trainer != null) { - TrainerPokemong trainerPokemong = new TrainerPokemong(); - trainerPokemong.setId(pokemong.getId()); - trainerPokemong.setNickname(pokemong.getNickname()); - trainerPokemong.setSpecies(pokemong.getSpecies()); - trainer.getPokemongs() - .add(trainerPokemong); - trainerService.updateOne(trainer); - } - return persistedPokemong; - } - - @Override - public void deleteOneById(String id) { - Pokemong pokemong = getOneById(id); - if (pokemong != null && pokemong.getTrainer() != null) { + String trainerId = pokemong.getTrainer(); + if (trainerId != null) { Trainer trainer = trainerService.getOneById(pokemong.getTrainer()); if (trainer != null) { - trainer.getPokemongs() - .removeIf(trainerPokemong -> trainerPokemong.getId() - .equals(id)); + TrainerPokemong trainerPokemong = new TrainerPokemong(); + trainerPokemong.setId(pokemong.getId()); + trainerPokemong.setNickname(pokemong.getNickname()); + trainerPokemong.setSpecies(pokemong.getSpecies()); + trainer.addPokemong(trainerPokemong); trainerService.updateOne(trainer); } } - super.deleteOneById(id); - } - - @Override - @Nullable - public Pokemong updateOne(@NotNull Pokemong pokemong) { - Pokemong existingPokemong = pokemongRepository.findById(pokemong.getId()); - if (existingPokemong != null) { - boolean nicknameChanged = !Objects.equals(existingPokemong.getNickname(), pokemong.getNickname()); - boolean evoStageChanged = !Objects.equals(existingPokemong.getEvoStage(), pokemong.getEvoStage()); - boolean evoTrackChanged = !Objects.equals(existingPokemong.getEvoTrack(), pokemong.getEvoTrack()); - - existingPokemong.setNickname(pokemong.getNickname()); - existingPokemong.setDob(pokemong.getDob()); - existingPokemong.setLevel(pokemong.getLevel()); - existingPokemong.setPokedexId(pokemong.getPokedexId()); - existingPokemong.setEvoStage(pokemong.getEvoStage()); - existingPokemong.setEvoTrack(pokemong.getEvoTrack()); - existingPokemong.setTrainer(pokemong.getTrainer()); - existingPokemong.setTypes(pokemong.getTypes()); - existingPokemong.setMoveSet(pokemong.getMoveSet()); - - pokemongRepository.persistOrUpdate(existingPokemong); - - if (nicknameChanged || evoStageChanged || evoTrackChanged) { - Trainer trainer = trainerService.getOneById(existingPokemong.getTrainer()); - if (trainer != null) { - TrainerPokemong trainerPokemong = trainer.getPokemongs() - .stream() - .filter(tp -> tp.getId() - .equals(existingPokemong.getId())) - .findFirst() - .orElse(null); - - if (trainerPokemong != null) { - if (nicknameChanged) { - trainerPokemong.setNickname(existingPokemong.getNickname()); - } - - if (evoStageChanged || evoTrackChanged) { - trainerPokemong.setSpecies(existingPokemong.getSpecies()); - } - - trainerService.updateOne(trainer); - } - } - } - } - return existingPokemong; + return persistedPokemong; } @Override @@ -140,19 +82,17 @@ public class PokemongService extends GenericService { errors.add("pokemong evo track was null or invalid"); } - List types = pokemong.getTypes(); + Set types = pokemong.getTypes(); if (types == null - || types.size() == 0 - || types.size() > 2) - { + || types.size() == 0 + || types.size() > 2) { errors.add("pokemong types was null or empty or had more than 2 types"); } Set moveSet = pokemong.getMoveSet(); if (moveSet == null) { errors.add("pokemong move set was null"); - } - else { + } else { if (moveSet.size() == 0 || moveSet.size() > 4) { errors.add("pokemong move set was empty or had more than 4 moves"); } @@ -171,11 +111,88 @@ public class PokemongService extends GenericService { } } + if (pokemong.getSchemaVersion() == null || + !Objects.equals(pokemong.getSchemaVersion(), Pokemong.LATEST_SCHEMA_VERSION)) { + errors.add( + "pokemong schema version was null or not the latest version: " + Pokemong.LATEST_SCHEMA_VERSION); + } + if (!errors.isEmpty()) { throw new NonValidEntityException("Validation errors: " + String.join(", ", errors)); } } + @Override + public void deleteOneById(String id) { + Pokemong pokemong = getOneById(id); + if (pokemong != null && pokemong.getTrainer() != null) { + Trainer trainer = trainerService.getOneById(pokemong.getTrainer()); + if (trainer != null) { + trainer.removePokemong(id); + trainerService.updateOne(trainer); + } + } + super.deleteOneById(id); + } + + @Override + @Nullable + public Pokemong updateOne(@NotNull Pokemong pokemong) { + super.updateOne(pokemong); + Pokemong existingPokemong = pokemongRepository.findById(pokemong.getId()); + if (existingPokemong != null) { + boolean nicknameChanged = !Objects.equals(existingPokemong.getNickname(), pokemong.getNickname()); + boolean evoStageChanged = !Objects.equals(existingPokemong.getEvoStage(), pokemong.getEvoStage()); + boolean evoTrackChanged = !Objects.equals(existingPokemong.getEvoTrack(), pokemong.getEvoTrack()); + + existingPokemong.setNickname(pokemong.getNickname()); + existingPokemong.setDob(pokemong.getDob()); + existingPokemong.setLevel(pokemong.getLevel()); + existingPokemong.setPokedexId(pokemong.getPokedexId()); + existingPokemong.setEvoStage(pokemong.getEvoStage()); + existingPokemong.setEvoTrack(pokemong.getEvoTrack()); + existingPokemong.setTrainer(pokemong.getTrainer()); + existingPokemong.setTypes(pokemong.getTypes()); + existingPokemong.setMoveSet(pokemong.getMoveSet()); + + pokemongRepository.persistOrUpdate(existingPokemong); + + if (nicknameChanged || evoStageChanged || evoTrackChanged) { + updateTrainerPokemong(existingPokemong, nicknameChanged, evoStageChanged, evoTrackChanged); + } + } + return existingPokemong; + } + + private void updateTrainerPokemong( + @NotNull Pokemong existingPokemong, + boolean nicknameChanged, + boolean evoStageChanged, + boolean evoTrackChanged + ) { + Trainer trainer = trainerService.getOneById(existingPokemong.getTrainer()); + if (trainer != null) { + TrainerPokemong trainerPokemong = + trainer.getPokemongs() + .stream() + .filter(tp -> tp.getId() + .equals(existingPokemong.getId())) + .findFirst() + .orElse(null); + + if (trainerPokemong != null) { + if (nicknameChanged) { + trainerPokemong.setNickname(existingPokemong.getNickname()); + } + + if (evoStageChanged || evoTrackChanged) { + trainerPokemong.setSpecies(existingPokemong.getSpecies()); + } + trainerService.updateOne(trainer); + } + } + } + public List findByMove(String id) { return pokemongRepository.findByMove(id); } @@ -190,13 +207,16 @@ public class PokemongService extends GenericService { return repository.existsById(pokemongId); } - public void batchUpdatePokemongTrainers(List trainerPokemongs, @Nullable String trainerId) { + public void batchUpdatePokemongTrainers(@NotNull Set trainerPokemongs, + @Nullable String trainerId) { + List pokemongsToUpdate = new ArrayList<>(); for (TrainerPokemong trainerPokemong : trainerPokemongs) { Pokemong pokemong = getOneById(trainerPokemong.getId()); - if (pokemong != null) { + if (pokemong != null && !Objects.equals(pokemong.getTrainer(), trainerId)) { pokemong.setTrainer(trainerId); - updateOne(pokemong); + pokemongsToUpdate.add(pokemong); } } + updateAll(pokemongsToUpdate); } } diff --git a/src/main/java/fr/uca/iut/services/TrainerService.java b/src/main/java/fr/uca/iut/services/TrainerService.java index 65d8a88..07724ac 100644 --- a/src/main/java/fr/uca/iut/services/TrainerService.java +++ b/src/main/java/fr/uca/iut/services/TrainerService.java @@ -3,7 +3,7 @@ package fr.uca.iut.services; import com.mongodb.lang.Nullable; import fr.uca.iut.entities.Pokemong; import fr.uca.iut.entities.Trainer; -import fr.uca.iut.entities.TrainerPokemong; +import fr.uca.iut.entities.denormalized.TrainerPokemong; import fr.uca.iut.repositories.TrainerRepository; import fr.uca.iut.utils.StringUtils; import fr.uca.iut.utils.exceptions.NonValidEntityException; @@ -12,8 +12,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; @ApplicationScoped public class TrainerService extends GenericService { @@ -33,38 +33,14 @@ public class TrainerService extends GenericService { public Trainer addOne(@NotNull Trainer trainer) { Trainer persistedTrainer = super.addOne(trainer); + // If this trainer gained pokemongs, that pokemong's ex-trainer if any needs to lose said pokemongs + transferNewlyArrivedTrainerPokemongs(new HashSet<>(), persistedTrainer.getPokemongs()); + // all owned pokemongs gain this trainer's reference pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), trainer.getId()); return persistedTrainer; } - @Override - public void deleteOneById(String id) { - Trainer trainer = getOneById(id); - - if (trainer != null) { - pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), null); - } - - super.deleteOneById(id); - } - - @Nullable - @Override - public Trainer updateOne(@NotNull Trainer trainer) { - Trainer existingTrainer = trainerRepository.findById(trainer.getId()); - if (existingTrainer != null) { - existingTrainer.setName(trainer.getName()); - existingTrainer.setDob(trainer.getDob()); - existingTrainer.setWins(trainer.getLosses()); - existingTrainer.setLosses(trainer.getLosses()); - existingTrainer.setPastOpponents(trainer.getPastOpponents()); - existingTrainer.setPokemongs(trainer.getPokemongs()); - trainerRepository.persistOrUpdate(existingTrainer); - } - return existingTrainer; - } - @Override public void validateOne(Trainer trainer) { @@ -92,45 +68,118 @@ public class TrainerService extends GenericService { if (pastOpponents == null) { errors.add("trainer past opponents collection was null"); - } - else { + } else { for (String trainerId : pastOpponents) { - if (StringUtils.isBlankStringOrNull(trainerId) || !trainerRepository.existsById(trainerId)) { - errors.add("trainer past opponents collection contained an invalid or unknown id"); + if (StringUtils.isBlankStringOrNull(trainerId)) { + errors.add("trainer past opponents collection contained an invalid id: " + trainerId); } } } - List pokemongs = trainer.getPokemongs(); + Set pokemongs = trainer.getPokemongs(); if (pokemongs == null) { errors.add("trainer pokemongs collection was null or invalid"); - } - else { + } else { for (TrainerPokemong pokemong : pokemongs) { String pokemongId = pokemong.getId(); if (StringUtils.isBlankStringOrNull(pokemongId) || !pokemongService.existsById(pokemongId)) { errors.add("pokemong with id " + pokemongId + " does not exist"); - } - else { + } else { if (!pokemongService.isEvoValid(pokemongId, pokemong.getSpecies())) { errors.add("pokemong with id " + pokemongId + " cannot be a " + - pokemong.getSpecies()); + pokemong.getSpecies()); } Pokemong pokemongBehind = pokemongService.getOneById(pokemongId); if (pokemong.getNickname() != null - && pokemongBehind != null - && !pokemong.getNickname() - .equals(pokemongBehind.getNickname())) - { + && pokemongBehind != null + && !pokemong.getNickname() + .equals(pokemongBehind.getNickname())) { errors.add("pokemong with id " + pokemongId + " already has a nickname"); } } } } + if (trainer.getSchemaVersion() == null || + !Objects.equals(trainer.getSchemaVersion(), Trainer.LATEST_SCHEMA_VERSION)) { + errors.add("trainer schema version was null or not the latest version: " + Trainer.LATEST_SCHEMA_VERSION); + } + if (!errors.isEmpty()) { throw new NonValidEntityException("Validation errors: " + String.join(", ", errors)); } } + + @Override + public void deleteOneById(String id) { + Trainer trainer = getOneById(id); + + if (trainer != null) { + pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), null); + } + + super.deleteOneById(id); + } + + @Nullable + @Override + public Trainer updateOne(@NotNull Trainer trainer) { + super.updateOne(trainer); + Trainer existingTrainer = trainerRepository.findById(trainer.getId()); + if (existingTrainer != null) { + Set oldPokemongs = existingTrainer.getPokemongs(); + + existingTrainer.setName(trainer.getName()); + existingTrainer.setDob(trainer.getDob()); + existingTrainer.setWins(trainer.getLosses()); + existingTrainer.setLosses(trainer.getLosses()); + existingTrainer.setPastOpponents(trainer.getPastOpponents()); + existingTrainer.setPokemongs(trainer.getPokemongs()); + trainerRepository.persistOrUpdate(existingTrainer); + + Set newPokemongs = trainer.getPokemongs(); + + // all old pokemongs who are not there anymore lose their trainer reference + pokemongService.batchUpdatePokemongTrainers( + oldPokemongs.stream() + .filter(tp -> !newPokemongs.contains(tp)) + .collect(Collectors.toSet()), + null); + // If this trainer gained a pokemong, that pokemong's ex-trainer if any needs to lose said pokemong + transferNewlyArrivedTrainerPokemongs(oldPokemongs, newPokemongs); + // all new pokemongs who were not there before gain this trainer's reference + pokemongService.batchUpdatePokemongTrainers( + newPokemongs.stream() + .filter(tp -> !oldPokemongs.contains(tp)) + .collect(Collectors.toSet()), + existingTrainer.getId()); + } + return existingTrainer; + } + + private void transferNewlyArrivedTrainerPokemongs( + @NotNull Set oldPokemongs, + @NotNull Set newPokemongs + ) { + List trainersToUpdate = new ArrayList<>(); + + for (TrainerPokemong newTrainerPokemong : newPokemongs) { + if (oldPokemongs.isEmpty() || !oldPokemongs.contains(newTrainerPokemong)) { + Pokemong pokemong = pokemongService.getOneById(newTrainerPokemong.getId()); + if (pokemong != null) { + String oldTrainerId = pokemong.getTrainer(); + // If the pokemong already had a trainer, remove it from the old trainer's pokemongs list + if (oldTrainerId != null) { + Trainer oldTrainer = getOneById(oldTrainerId); + if (oldTrainer != null) { + oldTrainer.removePokemong(newTrainerPokemong.getId()); + trainersToUpdate.add(oldTrainer); + } + } + } + } + } + updateAll(trainersToUpdate); + } } diff --git a/src/main/resources/META-INF/openapi.yaml b/src/main/resources/META-INF/openapi.yaml index b3192f0..158ad35 100644 --- a/src/main/resources/META-INF/openapi.yaml +++ b/src/main/resources/META-INF/openapi.yaml @@ -310,6 +310,7 @@ components: - evoTrack - types - moveSet + - schemaVersion properties: nickname: type: string @@ -347,6 +348,8 @@ components: maxItems: 4 items: $ref: '#/components/schemas/PokemongMove' + schemaVersion: + $ref: '#/components/schemas/SchemaVersion' Move: type: object @@ -356,6 +359,7 @@ components: - category - accuracy - type + - schemaVersion properties: name: type: string @@ -370,6 +374,8 @@ components: minimum: 0 type: $ref: '#/components/schemas/TypeName' + schemaVersion: + $ref: '#/components/schemas/SchemaVersion' Trainer: type: object @@ -380,6 +386,7 @@ components: - losses - pastOpponents - pokemongs + - schemaVersion properties: name: type: string @@ -402,6 +409,8 @@ components: type: array items: $ref: '#/components/schemas/TrainerPokemong' + schemaVersion: + $ref: '#/components/schemas/SchemaVersion' PokemongMove: type: object @@ -431,6 +440,11 @@ components: species: $ref: '#/components/schemas/PokemongName' + SchemaVersion: + type: integer + minimum: 1 + description: must be >= 1, and furthermore must be == latest schema version + MoveCategoryName: type: string enum: [