make it so 2 trainers can't share 1 pokemong
use sets where collection order doesn't matter and items should be unique
make services call entity validation, to prevent additions and updates that are side-effects from bypassing the validator
implement schema versioning and incremental migrations
pull/5/head
Alexis Drai 2 years ago
parent cf46b9c44d
commit 9832343bc1

@ -6,19 +6,71 @@ Instructions are [here](https://clientserveur-courses.clubinfo-clermont.fr/Notat
## About
A "Pokemong" is a playful term for a `MongoDB` pocket monster.
A "Pokemong" is a playful term for a MongoDB pocket monster.
The application is developed using the Quarkus framework and uses `MongoDB` as its database.
The application is developed using the Quarkus framework and uses MongoDB as its database.
This application is a RESTful service designed to emulate a basic Pokemong management system. It allows users to perform
CRUD operations on Pokemongs, trainers, moves, and types.
This application is a RESTful service designed to emulate a basic `Pokemong` management system. It allows users to
perform
CRUD operations on `Pokemongs`, `Trainers`, `Moves`, and `Types`.
### 🗂️ DCM
<img src="./docs/mcd.png" alt="Data Concept Model" title="Data Concept Model">
### 🧬 UML Class diagram
<img src="./docs/nosql_uml.png" alt="UML Class Diagram" title="UML Class Diagram">
### NoSQL Schema Versioning Strategy
This application uses MongoDB, a NoSQL database, which provides flexibility in our data model. While this flexibility
has
its advantages, it poses a unique challenge when we need to update our data model, specifically when we want to
introduce breaking changes in the existing schema.
We have adopted a schema versioning strategy to overcome this challenge and manage these changes efficiently.
#### Schema Versioning Pattern
Schema versioning is a pattern that involves tagging each document in a collection with a version number. This version
number corresponds to the schema of the document and is used to handle schema changes in the code that reads these
documents.
Each entity in our model extends a `GenericVersionedEntity` class, which includes a `schemaVersion` field. This field is
an integer that starts at 1 and is to be incremented by one with each schema change. Every change to the schema needs to
involve the schema version number being incremented.
#### Incremental Document Migration
When a document is read from the database, the version number in the document is checked. If the version number is less
than the current version, the document is updated to the current version, and the updated document is written back to
the database. This process effectively migrates the document to the current version.
In the example of the `Move` class, the codec's `decodeV1` method handles documents with a `schemaVersion` of less
than `2`. When it reads a document with this version, it updates the `schemaVersion` to `2`, and writes the updated
document back to the database.
```java
Move decodeV1(Document document){
// ...
// Increment the schemaVersion to the current version
move.setSchemaVersion(2);
// Save the updated Move object back to the database
moveRepository.persistOrUpdate(move);
// ...
}
```
This strategy allows for graceful schema evolution in a NoSQL environment. Instead of requiring all documents to be
migrated at once, which can be a time-consuming operation for large collections, it enables incremental document
migration. This approach also helps to avoid downtime during schema migration, as the application continues to function
correctly regardless of the document version. As documents are read, they are updated to the current schema version,
allowing the schema migration to happen gradually over time.
However, note that this strategy increases write operations to the database, which could affect application performance.
## Prep steps
### ♨️ Java version

@ -13,6 +13,7 @@ import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.types.ObjectId;
import org.jetbrains.annotations.NotNull;
public class MoveCodec extends GenericCodec<Move> {
private final Codec<Document> documentCodec;
@ -28,6 +29,8 @@ public class MoveCodec extends GenericCodec<Move> {
doc.put("_id", new ObjectId(move.getId()));
doc.put("schemaVersion", move.getSchemaVersion());
doc.put("name", move.getName());
doc.put("category", move.getCategory());
@ -55,12 +58,52 @@ public class MoveCodec extends GenericCodec<Move> {
@Override
public Move decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, decoderContext);
Integer schemaVersion = document.getInteger("schemaVersion");
return switch (schemaVersion) {
case 1 -> decodeV1(document);
case 2 -> decodeV2(document);
default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
};
}
private @NotNull Move decodeV1(@NotNull Document document) {
Move move = new Move();
move.setId(document.getObjectId("_id")
.toString());
move.setSchemaVersion(document.getInteger("schemaVersion"));
move.setName(document.getString("name"));
move.setCategory(MoveCategoryName.valueOf(document.getString("category")));
move.setPower(document.getInteger("power"));
move.setAccuracy(document.getInteger("accuracy"));
Document typeDoc = (Document) document.get("type");
move.setType(TypeCodecUtil.extractType(typeDoc));
// Read and discard the old pp field
Integer pp = document.getInteger("pp");
return move;
}
private @NotNull Move decodeV2(@NotNull Document document) {
Move move = new Move();
move.setId(document.getObjectId("_id")
.toString());
move.setSchemaVersion(document.getInteger("schemaVersion"));
move.setName(document.getString("name"));
move.setCategory(MoveCategoryName.valueOf(document.getString("category")));
@ -75,4 +118,5 @@ public class MoveCodec extends GenericCodec<Move> {
return move;
}
}

@ -36,6 +36,8 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
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<Pokemong> {
@Override
public Pokemong decode(BsonReader reader, DecoderContext decoderContext) {
Document document = documentCodec.decode(reader, decoderContext);
Integer schemaVersion = document.getInteger("schemaVersion");
return switch (schemaVersion) {
case 1 -> decodeV1(document);
default -> throw new IllegalArgumentException("Unsupported schema version: " + schemaVersion);
};
}
private @NotNull Pokemong decodeV1(@NotNull Document document) {
Pokemong pokemong = new Pokemong();
pokemong.setId(document.getObjectId("_id")
.toString());
pokemong.setSchemaVersion(document.getInteger("schemaVersion"));
pokemong.setNickname(document.getString("nickname"));
Date dob = document.getDate("dob");
@ -134,10 +148,10 @@ public class PokemongCodec extends GenericCodec<Pokemong> {
pokemong.setTrainer(trainerId.toString());
}
List<Type> types = document.getList("types", Document.class)
.stream()
.map(TypeCodecUtil::extractType)
.collect(Collectors.toList());
Set<Type> types = document.getList("types", Document.class)
.stream()
.map(TypeCodecUtil::extractType)
.collect(Collectors.toSet());
pokemong.setTypes(types);
Set<PokemongMove> moveSet = document.getList("moveSet", Document.class)

@ -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<Trainer> {
@ -34,6 +35,8 @@ public class TrainerCodec extends GenericCodec<Trainer> {
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<Trainer> {
@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<Trainer> {
.collect(Collectors.toList());
trainer.setPastOpponents(pastOpponentsIds);
List<TrainerPokemong> pokemongList = document
Set<TrainerPokemong> pokemongList = document
.getList("pokemongs", Document.class)
.stream()
.map(pokemongDoc -> {
@ -111,7 +127,7 @@ public class TrainerCodec extends GenericCodec<Trainer> {
pokemong.setSpecies(PokemongName.valueOf(pokemongDoc.getString("species")));
return pokemong;
})
.collect(Collectors.toList());
.collect(Collectors.toSet());
trainer.setPokemongs(pokemongList);
return trainer;
}

@ -47,7 +47,6 @@ public abstract class GenericController<T extends GenericEntity> {
public Response createOne(T entity) {
try {
service.validateOne(entity);
T newEntity = service.addOne(entity);
return Response.status(Response.Status.CREATED)
@ -66,7 +65,6 @@ public abstract class GenericController<T extends GenericEntity> {
@Consumes(MediaType.APPLICATION_JSON)
public Response updateOne(@PathParam("id") String id, T entity) {
try {
service.validateOne(entity);
entity.setId(id);
T updatedEntity = service.updateOne(entity);

@ -0,0 +1,21 @@
package fr.uca.iut.entities;
/**
* The strategy for incrementing the schema version number is simple.
* <br><br>
* `schemaVersion` will have to start at 1, and need to be incremented by one at each schema change.
* <br><br>
* Every change to the schema needs to involve the schema version number being incremented.
*/
public abstract class GenericVersionedEntity extends GenericEntity {
private Integer schemaVersion;
public Integer getSchemaVersion() {
return schemaVersion;
}
public void setSchemaVersion(Integer schemaVersion) {
this.schemaVersion = schemaVersion;
}
}

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

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

@ -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<String> pastOpponents;
private List<TrainerPokemong> pokemongs;
private Set<TrainerPokemong> pokemongs;
public Trainer() {}
@ -58,11 +60,19 @@ public class Trainer extends GenericEntity {
this.pastOpponents = pastOpponents;
}
public List<TrainerPokemong> getPokemongs() {
return Collections.unmodifiableList(pokemongs);
public Set<TrainerPokemong> getPokemongs() {
return Collections.unmodifiableSet(pokemongs);
}
public void setPokemongs(List<TrainerPokemong> pokemongs) {
public void setPokemongs(Set<TrainerPokemong> pokemongs) {
this.pokemongs = pokemongs;
}
public void addPokemong(TrainerPokemong trainerPokemong) {
pokemongs.add(trainerPokemong);
}
public void removePokemong(TrainerPokemong trainerPokemong) {
pokemongs.remove(trainerPokemong);
}
}

@ -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<TypeName> getWeakAgainst() {
return Collections.unmodifiableList(weakAgainst);
return weakAgainst;
}
public void setWeakAgainst(List<TypeName> weakAgainst) {
@ -31,7 +30,7 @@ public class Type {
}
public List<TypeName> getEffectiveAgainst() {
return Collections.unmodifiableList(effectiveAgainst);
return effectiveAgainst;
}
public void setEffectiveAgainst(List<TypeName> effectiveAgainst) {

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

@ -13,6 +13,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@ApplicationScoped
public class MoveService extends GenericService<Move> {
@ -27,19 +29,70 @@ public class MoveService extends GenericService<Move> {
setRepository(moveRepository);
}
@Override
public void validateOne(Move move) {
super.validateOne(move);
List<String> 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<Move> getAll() {
return super.getAll()
.stream()
.map(this::migrateToV2)
.collect(Collectors.toList());
}
@Override
public void deleteOneById(String id) {
super.deleteOneById(id);
List<Pokemong> pokemongs = pokemongService.findByMove(id);
for (Pokemong pokemong : pokemongs) {
pokemong.removeMove(id);
pokemongService.updateOne(pokemong);
}
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<Move> {
return existingMove;
}
@Override
public void validateOne(Move move) {
super.validateOne(move);
List<String> 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) {

@ -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<Pokemong> {
@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<Pokemong> {
errors.add("pokemong evo track was null or invalid");
}
List<Type> types = pokemong.getTypes();
Set<Type> types = pokemong.getTypes();
if (types == null
|| types.size() == 0
|| types.size() > 2)
@ -174,11 +113,82 @@ public class PokemongService extends GenericService<Pokemong> {
}
}
if (pokemong.getSchemaVersion() == null ||
!Objects.equals(pokemong.getSchemaVersion(), Pokemong.LATEST_SCHEMA_VERSION))
{
errors.add(
"pokemong schema version was null or not the latest version: " + Pokemong.LATEST_SCHEMA_VERSION);
}
if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
}
}
@Override
public void deleteOneById(String id) {
Pokemong pokemong = getOneById(id);
if (pokemong != null && pokemong.getTrainer() != null) {
Trainer trainer = trainerService.getOneById(pokemong.getTrainer());
if (trainer != null) {
trainer.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<Pokemong> findByMove(String id) {
return pokemongRepository.findByMove(id);
}
@ -193,10 +203,12 @@ public class PokemongService extends GenericService<Pokemong> {
return repository.existsById(pokemongId);
}
public void batchUpdatePokemongTrainers(@NotNull List<TrainerPokemong> trainerPokemongs, @Nullable String trainerId) {
public void batchUpdatePokemongTrainers(@NotNull Set<TrainerPokemong> 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);
}

@ -12,8 +12,8 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
@ApplicationScoped
public class TrainerService extends GenericService<Trainer> {
@ -33,38 +33,14 @@ public class TrainerService extends GenericService<Trainer> {
public Trainer addOne(@NotNull Trainer trainer) {
Trainer persistedTrainer = super.addOne(trainer);
// If this trainer gained pokemongs, that pokemong's ex-trainer if any needs to lose said 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<Trainer> {
}
else {
for (String trainerId : pastOpponents) {
if (StringUtils.isBlankStringOrNull(trainerId) || !trainerRepository.existsById(trainerId)) {
errors.add("trainer past opponents collection contained an invalid or unknown id");
if (StringUtils.isBlankStringOrNull(trainerId)) {
errors.add("trainer past opponents collection contained an invalid id: " + trainerId);
}
}
}
List<TrainerPokemong> pokemongs = trainer.getPokemongs();
Set<TrainerPokemong> pokemongs = trainer.getPokemongs();
if (pokemongs == null) {
errors.add("trainer pokemongs collection was null or invalid");
@ -129,8 +105,84 @@ public class TrainerService extends GenericService<Trainer> {
}
}
if (trainer.getSchemaVersion() == null ||
!Objects.equals(trainer.getSchemaVersion(), Trainer.LATEST_SCHEMA_VERSION))
{
errors.add("trainer schema version was null or not the latest version: " + Trainer.LATEST_SCHEMA_VERSION);
}
if (!errors.isEmpty()) {
throw new NonValidEntityException("Validation errors: " + String.join(", ", errors));
}
}
@Override
public void deleteOneById(String id) {
Trainer trainer = getOneById(id);
if (trainer != null) {
pokemongService.batchUpdatePokemongTrainers(trainer.getPokemongs(), null);
}
super.deleteOneById(id);
}
@Nullable
@Override
public Trainer updateOne(@NotNull Trainer trainer) {
super.updateOne(trainer);
Trainer existingTrainer = trainerRepository.findById(trainer.getId());
if (existingTrainer != null) {
Set<TrainerPokemong> oldPokemongs = existingTrainer.getPokemongs();
existingTrainer.setName(trainer.getName());
existingTrainer.setDob(trainer.getDob());
existingTrainer.setWins(trainer.getLosses());
existingTrainer.setLosses(trainer.getLosses());
existingTrainer.setPastOpponents(trainer.getPastOpponents());
existingTrainer.setPokemongs(trainer.getPokemongs());
trainerRepository.persistOrUpdate(existingTrainer);
Set<TrainerPokemong> newPokemongs = trainer.getPokemongs();
// all old pokemongs who are not there anymore lose their trainer reference
pokemongService.batchUpdatePokemongTrainers(
oldPokemongs.stream()
.filter(tp -> !newPokemongs.contains(tp))
.collect(Collectors.toSet()),
null);
// If this trainer gained a pokemong, that pokemong's ex-trainer if any needs to lose said pokemong
transferNewlyArrivedTrainerPokemongs(oldPokemongs, newPokemongs);
// all new pokemongs who were not there before gain this trainer's reference
pokemongService.batchUpdatePokemongTrainers(
newPokemongs.stream()
.filter(tp -> !oldPokemongs.contains(tp))
.collect(Collectors.toSet()),
existingTrainer.getId());
}
return existingTrainer;
}
private void transferNewlyArrivedTrainerPokemongs(
@NotNull Set<TrainerPokemong> oldPokemongs,
@NotNull Set<TrainerPokemong> newPokemongs
)
{
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);
}
}
}
}
}
}
}

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

Loading…
Cancel
Save