diff --git a/README.md b/README.md
index e82d5e6..c45f0a6 100644
--- a/README.md
+++ b/README.md
@@ -6,19 +6,71 @@ Instructions are [here](https://clientserveur-courses.clubinfo-clermont.fr/Notat
## About
-A "Pokemong" is a playful term for a `MongoDB` pocket monster.
+A "Pokemong" is a playful term for a MongoDB pocket monster.
-The application is developed using the Quarkus framework and uses `MongoDB` as its database.
+The application is developed using the Quarkus framework and uses MongoDB as its database.
-This application is a RESTful service designed to emulate a basic Pokemong management system. It allows users to perform
-CRUD operations on Pokemongs, trainers, moves, and types.
+This application is a RESTful service designed to emulate a basic `Pokemong` management system. It allows users to
+perform
+CRUD operations on `Pokemongs`, `Trainers`, `Moves`, and `Types`.
### 🗂️ DCM
+
### 🧬 UML Class diagram
+
+### NoSQL Schema Versioning Strategy
+
+This application uses MongoDB, a NoSQL database, which provides flexibility in our data model. While this flexibility
+has
+its advantages, it poses a unique challenge when we need to update our data model, specifically when we want to
+introduce breaking changes in the existing schema.
+
+We have adopted a schema versioning strategy to overcome this challenge and manage these changes efficiently.
+
+#### Schema Versioning Pattern
+
+Schema versioning is a pattern that involves tagging each document in a collection with a version number. This version
+number corresponds to the schema of the document and is used to handle schema changes in the code that reads these
+documents.
+
+Each entity in our model extends a `GenericVersionedEntity` class, which includes a `schemaVersion` field. This field is
+an integer that starts at 1 and is to be incremented by one with each schema change. Every change to the schema needs to
+involve the schema version number being incremented.
+
+#### Incremental Document Migration
+
+When a document is read from the database, the version number in the document is checked. If the version number is less
+than the current version, the document is updated to the current version, and the updated document is written back to
+the database. This process effectively migrates the document to the current version.
+
+In the example of the `Move` class, the codec's `decodeV1` method handles documents with a `schemaVersion` of less
+than `2`. When it reads a document with this version, it updates the `schemaVersion` to `2`, and writes the updated
+document back to the database.
+
+```java
+Move decodeV1(Document document){
+ // ...
+ // Increment the schemaVersion to the current version
+ move.setSchemaVersion(2);
+
+ // Save the updated Move object back to the database
+ moveRepository.persistOrUpdate(move);
+ // ...
+ }
+```
+
+This strategy allows for graceful schema evolution in a NoSQL environment. Instead of requiring all documents to be
+migrated at once, which can be a time-consuming operation for large collections, it enables incremental document
+migration. This approach also helps to avoid downtime during schema migration, as the application continues to function
+correctly regardless of the document version. As documents are read, they are updated to the current schema version,
+allowing the schema migration to happen gradually over time.
+
+However, note that this strategy increases write operations to the database, which could affect application performance.
+
## Prep steps
### ♨️ Java version
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 ba61209..9b1be5d 100644
--- a/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/move/MoveCodec.java
@@ -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 {
private final Codec documentCodec;
@@ -28,6 +29,8 @@ public class MoveCodec extends GenericCodec {
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 {
@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 {
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 a4bdd36..c2137b5 100644
--- a/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/pokemong/PokemongCodec.java
@@ -36,6 +36,8 @@ public class PokemongCodec extends GenericCodec {
doc.put("_id", new ObjectId(pokemong.getId()));
+ doc.put("schemaVersion", pokemong.getSchemaVersion());
+
doc.put("nickname", pokemong.getNickname());
doc.put("dob",
@@ -103,11 +105,23 @@ 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());
+ pokemong.setSchemaVersion(document.getInteger("schemaVersion"));
+
pokemong.setNickname(document.getString("nickname"));
Date dob = document.getDate("dob");
@@ -134,10 +148,10 @@ 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)
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 d37e723..0f5a7d7 100644
--- a/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java
+++ b/src/main/java/fr/uca/iut/codecs/trainer/TrainerCodec.java
@@ -18,6 +18,7 @@ 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 {
@@ -34,6 +35,8 @@ public class TrainerCodec extends GenericCodec {
doc.put("_id", new ObjectId(trainer.getId()));
+ doc.put("schemaVersion", trainer.getSchemaVersion());
+
doc.put("name", trainer.getName());
LocalDate dob = trainer.getDob();
@@ -77,11 +80,24 @@ public class TrainerCodec extends GenericCodec {
@Override
public Trainer 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);
+ };
+ }
+
+ @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");
@@ -101,7 +117,7 @@ public class TrainerCodec extends GenericCodec {
.collect(Collectors.toList());
trainer.setPastOpponents(pastOpponentsIds);
- List pokemongList = document
+ Set pokemongList = document
.getList("pokemongs", Document.class)
.stream()
.map(pokemongDoc -> {
@@ -111,7 +127,7 @@ public class TrainerCodec extends GenericCodec {
pokemong.setSpecies(PokemongName.valueOf(pokemongDoc.getString("species")));
return pokemong;
})
- .collect(Collectors.toList());
+ .collect(Collectors.toSet());
trainer.setPokemongs(pokemongList);
return trainer;
}
diff --git a/src/main/java/fr/uca/iut/controllers/GenericController.java b/src/main/java/fr/uca/iut/controllers/GenericController.java
index 70b30d2..ddd16ec 100644
--- a/src/main/java/fr/uca/iut/controllers/GenericController.java
+++ b/src/main/java/fr/uca/iut/controllers/GenericController.java
@@ -47,7 +47,6 @@ public abstract class GenericController {
public Response createOne(T entity) {
try {
- service.validateOne(entity);
T newEntity = service.addOne(entity);
return Response.status(Response.Status.CREATED)
@@ -66,7 +65,6 @@ 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);
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 b25cd45..050396c 100644
--- a/src/main/java/fr/uca/iut/entities/Move.java
+++ b/src/main/java/fr/uca/iut/entities/Move.java
@@ -3,8 +3,9 @@ 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;
diff --git a/src/main/java/fr/uca/iut/entities/Pokemong.java b/src/main/java/fr/uca/iut/entities/Pokemong.java
index 3645a6e..2c30cd8 100644
--- a/src/main/java/fr/uca/iut/entities/Pokemong.java
+++ b/src/main/java/fr/uca/iut/entities/Pokemong.java
@@ -10,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;
@@ -22,7 +24,7 @@ public class Pokemong extends GenericEntity {
private List evoTrack;
@Nullable
private String trainer;
- private List types;
+ private Set types;
/**
* pokemong.moveSet: [{_id: ObjectId, name: String}]
@@ -73,11 +75,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;
}
@@ -111,7 +113,7 @@ public class Pokemong extends GenericEntity {
}
public List getEvoTrack() {
- return evoTrack;
+ return Collections.unmodifiableList(evoTrack);
}
public Integer getEvoStage() {
diff --git a/src/main/java/fr/uca/iut/entities/Trainer.java b/src/main/java/fr/uca/iut/entities/Trainer.java
index ea2fef7..a8dc22d 100644
--- a/src/main/java/fr/uca/iut/entities/Trainer.java
+++ b/src/main/java/fr/uca/iut/entities/Trainer.java
@@ -5,16 +5,18 @@ 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() {}
@@ -58,11 +60,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(TrainerPokemong trainerPokemong) {
+ pokemongs.remove(trainerPokemong);
+ }
}
diff --git a/src/main/java/fr/uca/iut/entities/embedded/Type.java b/src/main/java/fr/uca/iut/entities/embedded/Type.java
index 5af9d0d..452a6bd 100644
--- a/src/main/java/fr/uca/iut/entities/embedded/Type.java
+++ b/src/main/java/fr/uca/iut/entities/embedded/Type.java
@@ -2,7 +2,6 @@ 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;
@@ -23,7 +22,7 @@ public class Type {
}
public List getWeakAgainst() {
- return Collections.unmodifiableList(weakAgainst);
+ return weakAgainst;
}
public void setWeakAgainst(List weakAgainst) {
@@ -31,7 +30,7 @@ public class Type {
}
public List getEffectiveAgainst() {
- return Collections.unmodifiableList(effectiveAgainst);
+ return effectiveAgainst;
}
public void setEffectiveAgainst(List effectiveAgainst) {
diff --git a/src/main/java/fr/uca/iut/services/GenericService.java b/src/main/java/fr/uca/iut/services/GenericService.java
index 3bd1435..6f2ab6f 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,12 @@ public abstract class GenericService {
}
}
- @Nullable
- public abstract T updateOne(@NotNull T entity);
-
/**
* Override me and start with `super.validateOne(entity);`
*/
- 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;
}
}
diff --git a/src/main/java/fr/uca/iut/services/MoveService.java b/src/main/java/fr/uca/iut/services/MoveService.java
index ed30c0e..24f00b4 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,19 +29,70 @@ public class MoveService extends GenericService {
setRepository(moveRepository);
}
+ @Override
+ public void validateOne(Move move) {
+
+ super.validateOne(move);
+
+ List errors = new ArrayList<>();
+
+ if (StringUtils.isBlankStringOrNull(move.getName())) {
+ errors.add("move name was null, blank or empty");
+ }
+
+ if (move.getPower() == null || move.getPower() < 0) {
+ errors.add("move power was null or negative");
+ }
+
+ if (move.getCategory() == null) {
+ errors.add("move category was null or invalid");
+ }
+
+ if (move.getAccuracy() == null || move.getAccuracy() < 0) {
+ errors.add("move accuracy was null or negative");
+ }
+
+ if (move.getType() == null) {
+ 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) {
- super.deleteOneById(id);
List pokemongs = pokemongService.findByMove(id);
for (Pokemong pokemong : pokemongs) {
pokemong.removeMove(id);
pokemongService.updateOne(pokemong);
}
+ 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()
@@ -62,36 +115,20 @@ public class MoveService extends GenericService {
return existingMove;
}
- @Override
- public void validateOne(Move move) {
-
- super.validateOne(move);
-
- List errors = new ArrayList<>();
-
- if (StringUtils.isBlankStringOrNull(move.getName())) {
- errors.add("move name was null, blank or empty");
- }
-
- if (move.getPower() == null || move.getPower() < 0) {
- errors.add("move power was null or negative");
- }
-
- if (move.getCategory() == null) {
- errors.add("move category was null or invalid");
- }
-
- if (move.getAccuracy() == null || move.getAccuracy() < 0) {
- errors.add("move accuracy was null or negative");
- }
-
- if (move.getType() == null) {
- errors.add("move type was null or invalid");
- }
-
- if (!errors.isEmpty()) {
- throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
+ /**
+ * 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) {
diff --git a/src/main/java/fr/uca/iut/services/PokemongService.java b/src/main/java/fr/uca/iut/services/PokemongService.java
index c80886c..a047ef4 100644
--- a/src/main/java/fr/uca/iut/services/PokemongService.java
+++ b/src/main/java/fr/uca/iut/services/PokemongService.java
@@ -1,7 +1,8 @@
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;
@@ -39,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
@@ -143,7 +82,7 @@ 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)
@@ -174,11 +113,82 @@ 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.getPokemongs()
+ .removeIf(trainerPokemong -> trainerPokemong.getId()
+ .equals(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) {
+ 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;
+ }
+
public List findByMove(String id) {
return pokemongRepository.findByMove(id);
}
@@ -193,10 +203,12 @@ public class PokemongService extends GenericService {
return repository.existsById(pokemongId);
}
- public void batchUpdatePokemongTrainers(@NotNull List trainerPokemongs, @Nullable String trainerId) {
+ public void batchUpdatePokemongTrainers(@NotNull Set trainerPokemongs,
+ @Nullable String trainerId)
+ {
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);
}
diff --git a/src/main/java/fr/uca/iut/services/TrainerService.java b/src/main/java/fr/uca/iut/services/TrainerService.java
index 5817a99..82ccc30 100644
--- a/src/main/java/fr/uca/iut/services/TrainerService.java
+++ b/src/main/java/fr/uca/iut/services/TrainerService.java
@@ -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 pokemong
+ 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) {
@@ -95,13 +71,13 @@ public class TrainerService extends GenericService {
}
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");
@@ -129,8 +105,84 @@ public class TrainerService extends GenericService {
}
}
+ 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
+ )
+ {
+ for (TrainerPokemong tp : newPokemongs) {
+ if (oldPokemongs.isEmpty() || !oldPokemongs.contains(tp)) {
+ Pokemong pokemong = pokemongService.getOneById(tp.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(tp);
+ updateOne(oldTrainer);
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/src/main/resources/META-INF/openapi.yaml b/src/main/resources/META-INF/openapi.yaml
index b3192f0..86aea97 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: [