From 3ce1215487c6c394725a6d5b66343c4315319ce6 Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Sat, 21 Feb 2026 19:50:17 +0100 Subject: [PATCH] Update dependencies, add character and scenario management features - Upgrade Quarkus and OpenAPI generator versions in pom.xml. - Introduce CharacterService and ScenarioService for managing character and scenario templates. - Implement CharacterEntity and ScenarioEntity JPA entities with corresponding repositories. - Add RESTful APIs for listing and retrieving characters and scenarios. - Create JSON converter for persisting lists of strings in the database. - Update OpenAPI specification to include new endpoints for character and scenario management. - Add Liquibase migration scripts for character and scenario tables. - Configure application settings for Hibernate ORM and database generation. --- .vscode/settings.json | 3 + pom.xml | 20 +- .../roleplay/business/CharacterService.java | 69 +++++ .../business/InMemorySessionService.java | 32 +- .../roleplay/business/ScenarioService.java | 83 +++++ .../roleplay/data/CharacterEntity.java | 148 +++++++++ .../roleplay/data/CharacterRepository.java | 33 ++ .../data/JsonListStringConverter.java | 45 +++ .../data/ScenarioCharacterEntity.java | 115 +++++++ .../neitzel/roleplay/data/ScenarioEntity.java | 116 +++++++ .../roleplay/data/ScenarioRepository.java | 38 +++ .../roleplay/fascade/CharactersResource.java | 39 +++ .../roleplay/fascade/ScenariosResource.java | 39 +++ src/main/resources/application.yml | 3 + src/main/resources/db/migration/changelog.xml | 2 + .../v001__scenarios_and_characters.xml | 103 +++++++ .../resources/openapi-roleplay-public-v1.yml | 143 +++++++++ .../Java/beanValidation.mustache | 14 + .../JavaJaxRS/beanValidation.mustache | 14 + .../openapi-templates/JavaJaxRS/pojo.mustache | 146 +++++++++ .../JavaJaxRS/propertyType.mustache | 1 + .../JavaJaxRS/spec/beanValidation.mustache | 1 + .../JavaJaxRS/spec/pojo.mustache | 289 ++++++++++++++++++ .../additionalEnumTypeAnnotations.mustache | 2 + .../additionalModelTypeAnnotations.mustache | 2 + .../additional_properties.mustache | 32 ++ .../beanValidatedType.mustache | 1 + .../openapi-templates/beanValidation.mustache | 1 + .../beanValidationCore.mustache | 20 ++ .../openapi-templates/enumClass.mustache | 56 ++++ .../openapi-templates/enumOuterClass.mustache | 64 ++++ .../generatedAnnotation.mustache | 1 + .../openapi-templates/model.mustache | 21 ++ .../resources/openapi-templates/pojo.mustache | 289 ++++++++++++++++++ .../typeInfoAnnotation.mustache | 8 + .../xmlPojoAnnotation.mustache | 8 + src/main/web/src/pages/StartPage.tsx | 60 +++- .../business/InMemorySessionServiceTest.java | 44 ++- .../business/ScenarioServiceTest.java | 127 ++++++++ tree.txt | 216 +++++++++++++ 40 files changed, 2428 insertions(+), 20 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/main/java/de/neitzel/roleplay/business/CharacterService.java create mode 100644 src/main/java/de/neitzel/roleplay/business/ScenarioService.java create mode 100644 src/main/java/de/neitzel/roleplay/data/CharacterEntity.java create mode 100644 src/main/java/de/neitzel/roleplay/data/CharacterRepository.java create mode 100644 src/main/java/de/neitzel/roleplay/data/JsonListStringConverter.java create mode 100644 src/main/java/de/neitzel/roleplay/data/ScenarioCharacterEntity.java create mode 100644 src/main/java/de/neitzel/roleplay/data/ScenarioEntity.java create mode 100644 src/main/java/de/neitzel/roleplay/data/ScenarioRepository.java create mode 100644 src/main/java/de/neitzel/roleplay/fascade/CharactersResource.java create mode 100644 src/main/java/de/neitzel/roleplay/fascade/ScenariosResource.java create mode 100644 src/main/resources/db/migration/v001__scenarios_and_characters.xml create mode 100644 src/main/resources/openapi-templates/Java/beanValidation.mustache create mode 100644 src/main/resources/openapi-templates/JavaJaxRS/beanValidation.mustache create mode 100644 src/main/resources/openapi-templates/JavaJaxRS/pojo.mustache create mode 100644 src/main/resources/openapi-templates/JavaJaxRS/propertyType.mustache create mode 100644 src/main/resources/openapi-templates/JavaJaxRS/spec/beanValidation.mustache create mode 100644 src/main/resources/openapi-templates/JavaJaxRS/spec/pojo.mustache create mode 100644 src/main/resources/openapi-templates/additionalEnumTypeAnnotations.mustache create mode 100644 src/main/resources/openapi-templates/additionalModelTypeAnnotations.mustache create mode 100644 src/main/resources/openapi-templates/additional_properties.mustache create mode 100644 src/main/resources/openapi-templates/beanValidatedType.mustache create mode 100644 src/main/resources/openapi-templates/beanValidation.mustache create mode 100644 src/main/resources/openapi-templates/beanValidationCore.mustache create mode 100644 src/main/resources/openapi-templates/enumClass.mustache create mode 100644 src/main/resources/openapi-templates/enumOuterClass.mustache create mode 100644 src/main/resources/openapi-templates/generatedAnnotation.mustache create mode 100644 src/main/resources/openapi-templates/model.mustache create mode 100644 src/main/resources/openapi-templates/pojo.mustache create mode 100644 src/main/resources/openapi-templates/typeInfoAnnotation.mustache create mode 100644 src/main/resources/openapi-templates/xmlPojoAnnotation.mustache create mode 100644 src/test/java/de/neitzel/roleplay/business/ScenarioServiceTest.java create mode 100644 tree.txt diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1402fbe..404916d 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,14 @@ 3.2.5 io.quarkus.platform quarkus-bom - 3.15.3 + 3.31.2 ${quarkus.platform.version} + 2.20.1 + 2.20 1.18.42 5.10.3 5.12.0 - 7.11.0 + 7.13.0 1.15.1 v22.13.1 10.9.2 @@ -68,6 +70,10 @@ io.quarkus quarkus-liquibase + + io.quarkus + quarkus-hibernate-orm-panache + io.quarkus quarkus-rest-client-config @@ -135,6 +141,7 @@ + org.openapitools openapi-generator-maven-plugin @@ -148,6 +155,7 @@ ${project.basedir}/src/main/resources/openapi-roleplay-public-v1.yml jaxrs-spec + ${project.basedir}/src/main/resources/openapi-templates ${project.build.directory}/generated-sources/openapi de.neitzel.roleplay.generated.api de.neitzel.roleplay.fascade.model @@ -172,6 +180,7 @@ generate + ${skip.openapi.generate} ${project.basedir}/src/main/resources/openapi-roleplay-public-v1.yml typescript-fetch ${project.basedir}/src/main/web/src/api/generated @@ -207,8 +216,11 @@ maven-surefire-plugin ${maven.surefire.plugin.version} - false - -Dnet.bytebuddy.experimental=true + @{argLine} + + org.jboss.logmanager.LogManager + ${maven.home} + diff --git a/src/main/java/de/neitzel/roleplay/business/CharacterService.java b/src/main/java/de/neitzel/roleplay/business/CharacterService.java new file mode 100644 index 0000000..45992a6 --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/business/CharacterService.java @@ -0,0 +1,69 @@ +package de.neitzel.roleplay.business; + +import de.neitzel.roleplay.data.CharacterEntity; +import de.neitzel.roleplay.data.CharacterRepository; +import de.neitzel.roleplay.fascade.model.CharacterDefinition; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Business service for stored character templates. Maps entities to API DTOs. + */ +@ApplicationScoped +public class CharacterService { + + private final CharacterRepository characterRepository; + + @Inject + public CharacterService(final CharacterRepository characterRepository) { + this.characterRepository = characterRepository; + } + + /** + * Returns all stored characters as API definitions. + * + * @return list of character definitions, ordered by name + */ + public List listCharacters() { + return characterRepository.listAll().stream() + .map(CharacterService::toCharacterDefinition) + .collect(Collectors.toList()); + } + + /** + * Returns a single character by id, if present. + * + * @param id the character UUID + * @return the character definition or empty + */ + public Optional getCharacter(final UUID id) { + CharacterEntity entity = characterRepository.findByIdOptional(id); + return entity != null ? Optional.of(toCharacterDefinition(entity)) : Optional.empty(); + } + + /** + * Maps a character entity to the API CharacterDefinition. Uses entity id as string for API id. + */ + public static CharacterDefinition toCharacterDefinition(final CharacterEntity entity) { + CharacterDefinition def = new CharacterDefinition( + entity.getId().toString(), + entity.getName(), + entity.getRole() + ); + def.setBackstory(entity.getBackstory()); + def.setSpeakingStyle(entity.getSpeakingStyle()); + if (entity.getPersonalityTraits() != null && !entity.getPersonalityTraits().isEmpty()) { + def.setPersonalityTraits(entity.getPersonalityTraits()); + } + if (entity.getGoals() != null && !entity.getGoals().isEmpty()) { + def.setGoals(entity.getGoals()); + } + return def; + } +} diff --git a/src/main/java/de/neitzel/roleplay/business/InMemorySessionService.java b/src/main/java/de/neitzel/roleplay/business/InMemorySessionService.java index af11552..d7ec7dc 100644 --- a/src/main/java/de/neitzel/roleplay/business/InMemorySessionService.java +++ b/src/main/java/de/neitzel/roleplay/business/InMemorySessionService.java @@ -36,6 +36,7 @@ public class InMemorySessionService implements SessionService { private final OllamaClient ollamaClient; private final com.fasterxml.jackson.databind.ObjectMapper objectMapper; + private final ScenarioService scenarioService; /** * In-memory store mapping session IDs to their current state. @@ -45,14 +46,17 @@ public class InMemorySessionService implements SessionService { /** * Creates the service with required dependencies. * - * @param ollamaClient client for Ollama narrative and state-update calls - * @param objectMapper mapper to serialize turn context to JSON + * @param ollamaClient client for Ollama narrative and state-update calls + * @param objectMapper mapper to serialize turn context to JSON + * @param scenarioService service to resolve scenario by id from the database */ @Inject public InMemorySessionService(final OllamaClient ollamaClient, - final com.fasterxml.jackson.databind.ObjectMapper objectMapper) { + final com.fasterxml.jackson.databind.ObjectMapper objectMapper, + final ScenarioService scenarioService) { this.ollamaClient = ollamaClient; this.objectMapper = objectMapper; + this.scenarioService = scenarioService; } /** @@ -77,11 +81,12 @@ public class InMemorySessionService implements SessionService { 0 ); - if (request.getScenario() != null) { - session.setSituation(buildSituationFromScenario(request.getScenario())); - session.setCharacters(buildCharactersFromScenario(request.getScenario())); + ScenarioSetup scenario = resolveScenario(request); + if (scenario != null) { + session.setSituation(buildSituationFromScenario(scenario)); + session.setCharacters(buildCharactersFromScenario(scenario)); try { - String contextJson = objectMapper.writeValueAsString(OllamaContextBuilder.fromScenario(request.getScenario())); + String contextJson = objectMapper.writeValueAsString(OllamaContextBuilder.fromScenario(scenario)); String narrative = ollamaClient.generateNarrative(model, OllamaPrompts.INIT_NARRATIVE, contextJson); String userContentForCall2 = contextJson + "\n\nNarrative that was just generated:\n" + narrative; StateUpdateResponse stateUpdate = ollamaClient.generateStateUpdate(model, OllamaPrompts.STATE_EXTRACTION, userContentForCall2); @@ -98,6 +103,19 @@ public class InMemorySessionService implements SessionService { return session; } + /** + * Resolves the effective scenario: scenarioId from DB takes precedence over inline scenario. + * + * @param request the create session request + * @return the scenario to use, or null if none + */ + private ScenarioSetup resolveScenario(final CreateSessionRequest request) { + if (request.getScenarioId() != null) { + return scenarioService.getScenarioAsSetup(request.getScenarioId()).orElse(null); + } + return request.getScenario(); + } + /** * Builds initial situation state from the scenario setup. * diff --git a/src/main/java/de/neitzel/roleplay/business/ScenarioService.java b/src/main/java/de/neitzel/roleplay/business/ScenarioService.java new file mode 100644 index 0000000..b643873 --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/business/ScenarioService.java @@ -0,0 +1,83 @@ +package de.neitzel.roleplay.business; + +import de.neitzel.roleplay.data.ScenarioCharacterEntity; +import de.neitzel.roleplay.data.ScenarioEntity; +import de.neitzel.roleplay.data.ScenarioRepository; +import de.neitzel.roleplay.fascade.model.CharacterDefinition; +import de.neitzel.roleplay.fascade.model.ScenarioSetup; +import de.neitzel.roleplay.fascade.model.ScenarioSummary; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Business service for stored scenario templates. Maps entities to API DTOs. + */ +@ApplicationScoped +public class ScenarioService { + + private final ScenarioRepository scenarioRepository; + + @Inject + public ScenarioService(final ScenarioRepository scenarioRepository) { + this.scenarioRepository = scenarioRepository; + } + + /** + * Returns all stored scenarios as summaries. + * + * @return list of scenario summaries + */ + public List listScenarios() { + return scenarioRepository.listAll().stream() + .map(ScenarioService::toScenarioSummary) + .toList(); + } + + /** + * Returns the full scenario setup for the given id, if present. + * + * @param id the scenario UUID + * @return the scenario setup (setting, conflict, user character, AI characters) or empty + */ + public Optional getScenarioAsSetup(final UUID id) { + ScenarioEntity entity = scenarioRepository.findByIdWithCharacters(id); + return entity != null ? Optional.of(toScenarioSetup(entity)) : Optional.empty(); + } + + /** + * Maps a scenario entity to the list-summary DTO. + */ + public static ScenarioSummary toScenarioSummary(final ScenarioEntity entity) { + ScenarioSummary summary = new ScenarioSummary(entity.getId(), entity.getName()); + summary.setSetting(entity.getSetting()); + summary.setInitialConflict(entity.getInitialConflict()); + return summary; + } + + /** + * Maps a scenario entity (with characters loaded) to the full ScenarioSetup for session creation. + */ + public static ScenarioSetup toScenarioSetup(final ScenarioEntity entity) { + ScenarioSetup setup = new ScenarioSetup(); + setup.setSetting(entity.getSetting()); + setup.setInitialConflict(entity.getInitialConflict()); + List links = entity.getScenarioCharacters(); + if (links != null && !links.isEmpty()) { + for (ScenarioCharacterEntity link : links) { + CharacterDefinition def = CharacterService.toCharacterDefinition(link.getCharacter()); + if (link.isUserCharacter()) { + setup.setUserCharacter(def); + } else { + setup.addAiCharactersItem(def); + } + } + } + return setup; + } +} diff --git a/src/main/java/de/neitzel/roleplay/data/CharacterEntity.java b/src/main/java/de/neitzel/roleplay/data/CharacterEntity.java new file mode 100644 index 0000000..c9113b0 --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/data/CharacterEntity.java @@ -0,0 +1,148 @@ +package de.neitzel.roleplay.data; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.List; +import java.util.UUID; + +/** + * JPA entity for a reusable character definition stored in {@code rp_character}. + */ +@Entity +@Table(name = "rp_character") +public class CharacterEntity extends PanacheEntityBase { + + @Id + @Column(name = "id", length = 36, nullable = false, updatable = false) + private UUID id; + + @Column(name = "name", nullable = false, length = 255) + private String name; + + @Column(name = "role", nullable = false, length = 255) + private String role; + + @Column(name = "backstory", columnDefinition = "clob") + private String backstory; + + @Convert(converter = JsonListStringConverter.class) + @Column(name = "personality_traits", columnDefinition = "clob") + private List personalityTraits; + + @Column(name = "speaking_style", length = 1000) + private String speakingStyle; + + @Convert(converter = JsonListStringConverter.class) + @Column(name = "goals", columnDefinition = "clob") + private List goals; + + /** + * Default constructor for JPA. + */ + public CharacterEntity() { + } + + /** + * Returns the unique identifier of this character. + */ + public UUID getId() { + return id; + } + + /** + * Sets the unique identifier of this character. + */ + public void setId(final UUID id) { + this.id = id; + } + + /** + * Returns the display name of this character. + */ + public String getName() { + return name; + } + + /** + * Sets the display name of this character. + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Returns the narrative role of this character. + */ + public String getRole() { + return role; + } + + /** + * Sets the narrative role of this character. + */ + public void setRole(final String role) { + this.role = role; + } + + /** + * Returns the backstory text, or null if not set. + */ + public String getBackstory() { + return backstory; + } + + /** + * Sets the backstory text. + */ + public void setBackstory(final String backstory) { + this.backstory = backstory; + } + + /** + * Returns the list of personality trait strings; never null. + */ + public List getPersonalityTraits() { + return personalityTraits; + } + + /** + * Sets the list of personality trait strings. + */ + public void setPersonalityTraits(final List personalityTraits) { + this.personalityTraits = personalityTraits; + } + + /** + * Returns the speaking style description, or null if not set. + */ + public String getSpeakingStyle() { + return speakingStyle; + } + + /** + * Sets the speaking style description. + */ + public void setSpeakingStyle(final String speakingStyle) { + this.speakingStyle = speakingStyle; + } + + /** + * Returns the list of goal strings; never null. + */ + public List getGoals() { + return goals; + } + + /** + * Sets the list of goal strings. + */ + public void setGoals(final List goals) { + this.goals = goals; + } +} diff --git a/src/main/java/de/neitzel/roleplay/data/CharacterRepository.java b/src/main/java/de/neitzel/roleplay/data/CharacterRepository.java new file mode 100644 index 0000000..6496f63 --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/data/CharacterRepository.java @@ -0,0 +1,33 @@ +package de.neitzel.roleplay.data; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.UUID; + +/** + * Panache repository for {@link CharacterEntity}. Provides list-all and find-by-id access. + */ +@ApplicationScoped +public class CharacterRepository implements PanacheRepository { + + /** + * Returns all characters ordered by name. + */ + @Override + public List listAll() { + return list("ORDER BY name"); + } + + /** + * Finds a character by its UUID. + * + * @param id the character id + * @return the entity or null if not found + */ + public CharacterEntity findByIdOptional(final UUID id) { + return find("id", id).firstResult(); + } +} diff --git a/src/main/java/de/neitzel/roleplay/data/JsonListStringConverter.java b/src/main/java/de/neitzel/roleplay/data/JsonListStringConverter.java new file mode 100644 index 0000000..3f9b95f --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/data/JsonListStringConverter.java @@ -0,0 +1,45 @@ +package de.neitzel.roleplay.data; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.Collections; +import java.util.List; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * JPA converter that persists a {@link List} of {@link String} as JSON in a CLOB column. + */ +@Converter +public class JsonListStringConverter implements AttributeConverter, String> { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final TypeReference> LIST_TYPE = new TypeReference<>() {}; + + @Override + public String convertToDatabaseColumn(final List attribute) { + if (attribute == null || attribute.isEmpty()) { + return null; + } + try { + return MAPPER.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to serialize string list to JSON", e); + } + } + + @Override + public List convertToEntityAttribute(final String dbData) { + if (dbData == null || dbData.isBlank()) { + return Collections.emptyList(); + } + try { + return MAPPER.readValue(dbData, LIST_TYPE); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to deserialize JSON to string list", e); + } + } +} diff --git a/src/main/java/de/neitzel/roleplay/data/ScenarioCharacterEntity.java b/src/main/java/de/neitzel/roleplay/data/ScenarioCharacterEntity.java new file mode 100644 index 0000000..4e683a8 --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/data/ScenarioCharacterEntity.java @@ -0,0 +1,115 @@ +package de.neitzel.roleplay.data; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import java.util.UUID; + +/** + * Join entity linking a scenario to a character with role (user vs AI) and ordering. + */ +@Entity +@Table(name = "scenario_character") +public class ScenarioCharacterEntity extends PanacheEntityBase { + + @Id + @Column(name = "id", length = 36, nullable = false, updatable = false) + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "scenario_id", nullable = false) + private ScenarioEntity scenario; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "character_id", nullable = false) + private CharacterEntity character; + + @Column(name = "is_user_character", nullable = false) + private boolean userCharacter; + + @Column(name = "position", nullable = false) + private int position; + + /** + * Default constructor for JPA. + */ + public ScenarioCharacterEntity() { + } + + /** + * Returns the unique identifier of this link. + */ + public UUID getId() { + return id; + } + + /** + * Sets the unique identifier of this link. + */ + public void setId(final UUID id) { + this.id = id; + } + + /** + * Returns the scenario this link belongs to. + */ + public ScenarioEntity getScenario() { + return scenario; + } + + /** + * Sets the scenario this link belongs to. + */ + public void setScenario(final ScenarioEntity scenario) { + this.scenario = scenario; + } + + /** + * Returns the character referenced by this link. + */ + public CharacterEntity getCharacter() { + return character; + } + + /** + * Sets the character referenced by this link. + */ + public void setCharacter(final CharacterEntity character) { + this.character = character; + } + + /** + * Returns whether this slot is the user-controlled character. + */ + public boolean isUserCharacter() { + return userCharacter; + } + + /** + * Sets whether this slot is the user-controlled character. + */ + public void setUserCharacter(final boolean userCharacter) { + this.userCharacter = userCharacter; + } + + /** + * Returns the position for ordering (e.g. AI characters). + */ + public int getPosition() { + return position; + } + + /** + * Sets the position for ordering. + */ + public void setPosition(final int position) { + this.position = position; + } +} diff --git a/src/main/java/de/neitzel/roleplay/data/ScenarioEntity.java b/src/main/java/de/neitzel/roleplay/data/ScenarioEntity.java new file mode 100644 index 0000000..7f0c7fd --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/data/ScenarioEntity.java @@ -0,0 +1,116 @@ +package de.neitzel.roleplay.data; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.Table; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * JPA entity for a scenario template stored in {@code scenario}. + */ +@Entity +@Table(name = "scenario") +public class ScenarioEntity extends PanacheEntityBase { + + @Id + @Column(name = "id", length = 36, nullable = false, updatable = false) + private UUID id; + + @Column(name = "name", nullable = false, length = 255) + private String name; + + @Column(name = "setting", columnDefinition = "clob") + private String setting; + + @Column(name = "initial_conflict", columnDefinition = "clob") + private String initialConflict; + + @OneToMany(mappedBy = "scenario", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("isUserCharacter DESC, position ASC") + private List scenarioCharacters = new ArrayList<>(); + + /** + * Default constructor for JPA. + */ + public ScenarioEntity() { + } + + /** + * Returns the unique identifier of this scenario. + */ + public UUID getId() { + return id; + } + + /** + * Sets the unique identifier of this scenario. + */ + public void setId(final UUID id) { + this.id = id; + } + + /** + * Returns the human-readable name of this scenario. + */ + public String getName() { + return name; + } + + /** + * Sets the human-readable name of this scenario. + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Returns the setting description (place, time, atmosphere), or null if not set. + */ + public String getSetting() { + return setting; + } + + /** + * Sets the setting description. + */ + public void setSetting(final String setting) { + this.setting = setting; + } + + /** + * Returns the initial conflict text, or null if not set. + */ + public String getInitialConflict() { + return initialConflict; + } + + /** + * Sets the initial conflict text. + */ + public void setInitialConflict(final String initialConflict) { + this.initialConflict = initialConflict; + } + + /** + * Returns the list of scenario–character links (user character first, then AI by position). + */ + public List getScenarioCharacters() { + return scenarioCharacters; + } + + /** + * Sets the list of scenario–character links. + */ + public void setScenarioCharacters(final List scenarioCharacters) { + this.scenarioCharacters = scenarioCharacters; + } +} diff --git a/src/main/java/de/neitzel/roleplay/data/ScenarioRepository.java b/src/main/java/de/neitzel/roleplay/data/ScenarioRepository.java new file mode 100644 index 0000000..ef1e51f --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/data/ScenarioRepository.java @@ -0,0 +1,38 @@ +package de.neitzel.roleplay.data; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.UUID; + +/** + * Panache repository for {@link ScenarioEntity}. Provides list-all and find-by-id with characters loaded. + */ +@ApplicationScoped +public class ScenarioRepository implements PanacheRepository { + + /** + * Finds a scenario by its UUID and loads its scenario-character links and linked characters. + * + * @param id the scenario id + * @return the entity or null if not found + */ + public ScenarioEntity findByIdWithCharacters(final UUID id) { + ScenarioEntity scenario = find("id", id).firstResult(); + if (scenario != null) { + scenario.getScenarioCharacters().size(); + scenario.getScenarioCharacters().forEach(sc -> sc.getCharacter().getName()); + } + return scenario; + } + + /** + * Returns all scenarios ordered by name. + */ + @Override + public List listAll() { + return list("ORDER BY name"); + } +} diff --git a/src/main/java/de/neitzel/roleplay/fascade/CharactersResource.java b/src/main/java/de/neitzel/roleplay/fascade/CharactersResource.java new file mode 100644 index 0000000..30232be --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/fascade/CharactersResource.java @@ -0,0 +1,39 @@ +package de.neitzel.roleplay.fascade; + +import de.neitzel.roleplay.business.CharacterService; +import de.neitzel.roleplay.fascade.model.CharacterDefinition; +import de.neitzel.roleplay.fascade.model.CharacterListResponse; +import de.neitzel.roleplay.generated.api.CharactersApi; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; + +import java.util.UUID; + +/** + * JAX-RS resource that implements the {@link CharactersApi} interface generated from the OpenAPI spec. + */ +@ApplicationScoped +public class CharactersResource implements CharactersApi { + + private final CharacterService characterService; + + @Inject + public CharactersResource(final CharacterService characterService) { + this.characterService = characterService; + } + + @Override + public CharacterListResponse listCharacters() { + CharacterListResponse response = new CharacterListResponse(); + response.setCharacters(characterService.listCharacters()); + return response; + } + + @Override + public CharacterDefinition getCharacter(final UUID characterId) { + return characterService.getCharacter(characterId) + .orElseThrow(() -> new NotFoundException("No character found with id: " + characterId)); + } +} diff --git a/src/main/java/de/neitzel/roleplay/fascade/ScenariosResource.java b/src/main/java/de/neitzel/roleplay/fascade/ScenariosResource.java new file mode 100644 index 0000000..dff2f44 --- /dev/null +++ b/src/main/java/de/neitzel/roleplay/fascade/ScenariosResource.java @@ -0,0 +1,39 @@ +package de.neitzel.roleplay.fascade; + +import de.neitzel.roleplay.business.ScenarioService; +import de.neitzel.roleplay.fascade.model.ScenarioListResponse; +import de.neitzel.roleplay.fascade.model.ScenarioSetup; +import de.neitzel.roleplay.generated.api.ScenariosApi; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; + +import java.util.UUID; + +/** + * JAX-RS resource that implements the {@link ScenariosApi} interface generated from the OpenAPI spec. + */ +@ApplicationScoped +public class ScenariosResource implements ScenariosApi { + + private final ScenarioService scenarioService; + + @Inject + public ScenariosResource(final ScenarioService scenarioService) { + this.scenarioService = scenarioService; + } + + @Override + public ScenarioListResponse listScenarios() { + ScenarioListResponse response = new ScenarioListResponse(); + response.setScenarios(scenarioService.listScenarios()); + return response; + } + + @Override + public ScenarioSetup getScenario(final UUID scenarioId) { + return scenarioService.getScenarioAsSetup(scenarioId) + .orElseThrow(() -> new NotFoundException("No scenario found with id: " + scenarioId)); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5b5ef23..c08c4aa 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,9 @@ quarkus: url: jdbc:h2:mem:roleplay;DB_CLOSE_DELAY=-1 username: sa password: "" + hibernate-orm: + database: + generation: none liquibase: change-log: db/migration/changelog.xml migrate-at-start: true diff --git a/src/main/resources/db/migration/changelog.xml b/src/main/resources/db/migration/changelog.xml index 342d5a5..b775c7e 100644 --- a/src/main/resources/db/migration/changelog.xml +++ b/src/main/resources/db/migration/changelog.xml @@ -4,5 +4,7 @@ xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.24.xsd"> + + diff --git a/src/main/resources/db/migration/v001__scenarios_and_characters.xml b/src/main/resources/db/migration/v001__scenarios_and_characters.xml new file mode 100644 index 0000000..64e9b1d --- /dev/null +++ b/src/main/resources/db/migration/v001__scenarios_and_characters.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + Stores reusable character definitions. personality_traits and goals stored as JSON text. + + + + + + + + + + + + + + Stores scenario templates (setting and initial conflict). + + + + + + + + + + + + + + + + + + + + + Links scenarios to characters; position orders AI characters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/openapi-roleplay-public-v1.yml b/src/main/resources/openapi-roleplay-public-v1.yml index 865e5f3..43a7fe2 100644 --- a/src/main/resources/openapi-roleplay-public-v1.yml +++ b/src/main/resources/openapi-roleplay-public-v1.yml @@ -19,6 +19,10 @@ tags: description: Manage role-play sessions - name: turns description: Submit user actions within a session + - name: scenarios + description: List and retrieve saved scenario templates + - name: characters + description: List and retrieve saved character templates paths: @@ -43,6 +47,82 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /scenarios: + get: + operationId: listScenarios + summary: List saved scenarios + description: Returns all stored scenario templates for selection when starting a session. + tags: + - scenarios + responses: + "200": + description: List of scenario summaries. + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioListResponse' + + /scenarios/{scenarioId}: + get: + operationId: getScenario + summary: Get a scenario by id + description: Returns the full scenario setup (setting, conflict, characters) for the given id. + tags: + - scenarios + parameters: + - $ref: '#/components/parameters/ScenarioId' + responses: + "200": + description: Scenario found and returned. + content: + application/json: + schema: + $ref: '#/components/schemas/ScenarioSetup' + "404": + description: Scenario not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /characters: + get: + operationId: listCharacters + summary: List saved characters + description: Returns all stored character templates for selection when building a scenario. + tags: + - characters + responses: + "200": + description: List of character definitions. + content: + application/json: + schema: + $ref: '#/components/schemas/CharacterListResponse' + + /characters/{characterId}: + get: + operationId: getCharacter + summary: Get a character by id + description: Returns the full character definition for the given id. + tags: + - characters + parameters: + - $ref: '#/components/parameters/CharacterId' + responses: + "200": + description: Character found and returned. + content: + application/json: + schema: + $ref: '#/components/schemas/CharacterDefinition' + "404": + description: Character not found. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /sessions: post: operationId: createSession @@ -191,6 +271,22 @@ components: description: Unique identifier of the role-play session. schema: type: string + ScenarioId: + name: scenarioId + in: path + required: true + description: Unique identifier of the scenario (UUID). + schema: + type: string + format: uuid + CharacterId: + name: characterId + in: path + required: true + description: Unique identifier of the character (UUID). + schema: + type: string + format: uuid schemas: @@ -255,9 +351,56 @@ components: default: standard scenario: $ref: '#/components/schemas/ScenarioSetup' + scenarioId: + type: string + format: uuid + description: If set, the backend loads this scenario from the database and uses it instead of an inline scenario. required: - model + ScenarioSummary: + type: object + description: Summary of a stored scenario for list views. + properties: + id: + type: string + format: uuid + description: Unique identifier of the scenario. + name: + type: string + description: Human-readable scenario name. + setting: + type: string + description: Place, time, and atmosphere (optional in list). + initialConflict: + type: string + description: The hook or starting conflict (optional in list). + required: + - id + - name + + ScenarioListResponse: + type: object + description: Response containing all stored scenarios. + properties: + scenarios: + type: array + items: + $ref: '#/components/schemas/ScenarioSummary' + required: + - scenarios + + CharacterListResponse: + type: object + description: Response containing all stored characters. + properties: + characters: + type: array + items: + $ref: '#/components/schemas/CharacterDefinition' + required: + - characters + ScenarioSetup: type: object description: | diff --git a/src/main/resources/openapi-templates/Java/beanValidation.mustache b/src/main/resources/openapi-templates/Java/beanValidation.mustache new file mode 100644 index 0000000..e3d09ee --- /dev/null +++ b/src/main/resources/openapi-templates/Java/beanValidation.mustache @@ -0,0 +1,14 @@ +{{#required}} +{{^isReadOnly}} + @NotNull +{{/isReadOnly}} +{{/required}} +{{#isContainer}} +{{! Do not add @Valid on container; we use type-argument @Valid in the pojo (List<@Valid T>) to fix HV000271 }} +{{/isContainer}} +{{^isContainer}} +{{^isPrimitiveType}} + @Valid +{{/isPrimitiveType}} +{{/isContainer}} +{{>beanValidationCore}} diff --git a/src/main/resources/openapi-templates/JavaJaxRS/beanValidation.mustache b/src/main/resources/openapi-templates/JavaJaxRS/beanValidation.mustache new file mode 100644 index 0000000..e3d09ee --- /dev/null +++ b/src/main/resources/openapi-templates/JavaJaxRS/beanValidation.mustache @@ -0,0 +1,14 @@ +{{#required}} +{{^isReadOnly}} + @NotNull +{{/isReadOnly}} +{{/required}} +{{#isContainer}} +{{! Do not add @Valid on container; we use type-argument @Valid in the pojo (List<@Valid T>) to fix HV000271 }} +{{/isContainer}} +{{^isContainer}} +{{^isPrimitiveType}} + @Valid +{{/isPrimitiveType}} +{{/isContainer}} +{{>beanValidationCore}} diff --git a/src/main/resources/openapi-templates/JavaJaxRS/pojo.mustache b/src/main/resources/openapi-templates/JavaJaxRS/pojo.mustache new file mode 100644 index 0000000..4d9b365 --- /dev/null +++ b/src/main/resources/openapi-templates/JavaJaxRS/pojo.mustache @@ -0,0 +1,146 @@ +/** + * {{description}}{{^description}}{{classname}}{{/description}} + */{{#description}} +@ApiModel(description = "{{{.}}}"){{/description}} +{{#jackson}} +@JsonPropertyOrder({ +{{#vars}} + {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} +{{/vars}} +}) +{{/jackson}} +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}} +{{#vendorExtensions.x-class-extra-annotation}} +{{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}} +public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{#vars}} + {{#isEnum}} + {{^isContainer}} +{{>enumClass}} + + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} +{{>enumClass}} + + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#jackson}} + public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) + {{/jackson}} + {{#gson}} + public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; + @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) + {{/gson}} + {{#vendorExtensions.x-field-extra-annotation}} + {{{.}}} + {{/vendorExtensions.x-field-extra-annotation}} + private {{>propertyType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + + {{/vars}} + {{#vars}} + public {{classname}} {{name}}({{>propertyType}} {{name}}) { + this.{{name}} = {{name}}; + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + this.{{name}}.add({{name}}Item); + return this; + } + {{/isArray}} + {{#isMap}} + + public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + this.{{name}}.put(key, {{name}}Item); + return this; + } + {{/isMap}} + + /** + {{#description}} + * {{.}} + {{/description}} + {{^description}} + * Get {{name}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + * @return {{name}} + **/ + {{#vendorExtensions.x-extra-annotation}} + {{{vendorExtensions.x-extra-annotation}}} + {{/vendorExtensions.x-extra-annotation}} + {{#jackson}} + @JsonProperty(value = "{{baseName}}"{{#isReadOnly}}, access = JsonProperty.Access.READ_ONLY{{/isReadOnly}}{{#isWriteOnly}}, access = JsonProperty.Access.WRITE_ONLY{{/isWriteOnly}}) + {{/jackson}} + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") + {{#useBeanValidation}}{{#required}}{{^isReadOnly}} + @NotNull +{{/isReadOnly}}{{/required}}{{#isContainer}}{{! No @Valid on container; type has List<@Valid T> (HV000271) }}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}} + @Valid +{{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}}{{/useBeanValidation}} + public {{>propertyType}} {{getter}}() { + return {{name}}; + } + + {{#vendorExtensions.x-setter-extra-annotation}}{{{vendorExtensions.x-setter-extra-annotation}}} + {{/vendorExtensions.x-setter-extra-annotation}}public void {{setter}}({{>propertyType}} {{name}}) { + this.{{name}} = {{name}}; + } + + {{/vars}} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + {{classname}} {{classVarName}} = ({{classname}}) o;{{#hasVars}} + return {{#parent}}super.equals(o) && {{/parent}}{{#vars}}Objects.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} && + {{/-last}}{{#-last}};{{/-last}}{{/vars}}{{/hasVars}}{{^hasVars}}{{#parent}}return super.equals(o);{{/parent}}{{^parent}}return true;{{/parent}}{{/hasVars}} + } + + @Override + public int hashCode() { + return {{^hasVars}}{{#parent}}super.hashCode(){{/parent}}{{^parent}}1{{/parent}}{{/hasVars}}{{#hasVars}}Objects.hash({{#vars}}{{#parent}}super.hashCode(), {{/parent}}{{name}}{{^-last}}, {{/-last}}{{/vars}}){{/hasVars}}; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}}sb.append(" ").append(toIndentedString(super.toString())).append("\n");{{/parent}} + {{#vars}}sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n"); + {{/vars}}sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/resources/openapi-templates/JavaJaxRS/propertyType.mustache b/src/main/resources/openapi-templates/JavaJaxRS/propertyType.mustache new file mode 100644 index 0000000..97bb909 --- /dev/null +++ b/src/main/resources/openapi-templates/JavaJaxRS/propertyType.mustache @@ -0,0 +1 @@ +{{! Type with @Valid on type argument for containers (HV000271 fix). }}{{#isContainer}}{{#isArray}}{{#items.isPrimitiveType}}{{{datatypeWithEnum}}}{{/items.isPrimitiveType}}{{^items.isPrimitiveType}}{{#items.isEnum}}{{{datatypeWithEnum}}}{{/items.isEnum}}{{^items.isEnum}}List<@Valid {{{items.datatypeWithEnum}}}>{{/items.isEnum}}{{/items.isPrimitiveType}}{{/isArray}}{{^isArray}}{{#isMap}}{{#items.isPrimitiveType}}{{{datatypeWithEnum}}}{{/items.isPrimitiveType}}{{^items.isPrimitiveType}}{{#items.isEnum}}{{{datatypeWithEnum}}}{{/items.isEnum}}{{^items.isEnum}}Map{{/items.isEnum}}{{/items.isPrimitiveType}}{{/isMap}}{{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{/isMap}}{{/isArray}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}} diff --git a/src/main/resources/openapi-templates/JavaJaxRS/spec/beanValidation.mustache b/src/main/resources/openapi-templates/JavaJaxRS/spec/beanValidation.mustache new file mode 100644 index 0000000..8383c51 --- /dev/null +++ b/src/main/resources/openapi-templates/JavaJaxRS/spec/beanValidation.mustache @@ -0,0 +1 @@ +{{#required}}{{^isReadOnly}}@NotNull {{/isReadOnly}}{{/required}}{{#isContainer}}{{! Do not add @Valid on container; use type-argument @Valid (HV000271) }}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}}{{^isEnumOrRef}}@Valid {{/isEnumOrRef}}{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}} diff --git a/src/main/resources/openapi-templates/JavaJaxRS/spec/pojo.mustache b/src/main/resources/openapi-templates/JavaJaxRS/spec/pojo.mustache new file mode 100644 index 0000000..04480c4 --- /dev/null +++ b/src/main/resources/openapi-templates/JavaJaxRS/spec/pojo.mustache @@ -0,0 +1,289 @@ +{{#useSwaggerAnnotations}} +import io.swagger.annotations.*; +{{/useSwaggerAnnotations}} +{{#useSwaggerV3Annotations}} +import io.swagger.v3.oas.annotations.media.Schema; +{{/useSwaggerV3Annotations}} +import java.util.Objects; +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; +{{#additionalProperties}} +import java.util.Map; +import java.util.HashMap; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonFormat; +{{/additionalProperties}} +{{/jackson}} +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullable; +{{/openApiNullable}} +{{#withXml}} +import {{javaxPackage}}.xml.bind.annotation.XmlElement; +import {{javaxPackage}}.xml.bind.annotation.XmlRootElement; +import {{javaxPackage}}.xml.bind.annotation.XmlAccessType; +import {{javaxPackage}}.xml.bind.annotation.XmlAccessorType; +import {{javaxPackage}}.xml.bind.annotation.XmlType; +import {{javaxPackage}}.xml.bind.annotation.XmlEnum; +import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue; +{{/withXml}} + +{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{#description}}/** + * {{.}} + **/{{/description}} +{{#useSwaggerAnnotations}}{{#description}}@ApiModel(description = "{{{.}}}"){{/description}}{{/useSwaggerAnnotations}}{{#useSwaggerV3Annotations}} +@Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}){{/useSwaggerV3Annotations}}{{#useMicroProfileOpenAPIAnnotations}} +@org.eclipse.microprofile.openapi.annotations.media.Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}){{/useMicroProfileOpenAPIAnnotations}} +{{#jackson}} +@JsonTypeName("{{name}}") +{{#additionalProperties}} +@JsonFormat(shape=JsonFormat.Shape.OBJECT) +{{/additionalProperties}} +{{/jackson}} +{{>generatedAnnotation}}{{>additionalModelTypeAnnotations}}{{>xmlPojoAnnotation}} +{{#vendorExtensions.x-class-extra-annotation}} +{{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}} +public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{#vars}} + {{#isEnum}} + {{^isContainer}} + {{>enumClass}}{{! prevent indent}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} + {{>enumClass}}{{! prevent indent}} + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#vendorExtensions.x-field-extra-annotation}} + {{{.}}} + {{/vendorExtensions.x-field-extra-annotation}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); + {{/isContainer}} + {{^isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + {{/isContainer}} + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + private {{#useBeanValidation}}{{>beanValidatedType}}{{/useBeanValidation}}{{^useBeanValidation}}{{{datatypeWithEnum}}}{{/useBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{/vars}} + + {{#generateBuilders}} + {{^additionalProperties}} + protected {{classname}}({{classname}}Builder b) { + {{#parent}} + super(b); + {{/parent}} + {{#vars}} + this.{{name}} = b.{{name}}; + {{/vars}} + } + + {{/additionalProperties}} + {{/generateBuilders}} + public {{classname}}() { + } + + {{#jackson}} + {{#generateJsonCreator}} + {{#hasRequired}} + @JsonCreator + public {{classname}}( + {{#requiredVars}} + @JsonProperty(required = {{required}}, value = "{{baseName}}") {{>beanValidatedType}} {{name}}{{^-last}},{{/-last}} + {{/requiredVars}} + ) { + {{#parent}} + super( + {{#parentRequiredVars}} + {{name}}{{^-last}},{{/-last}} + {{/parentRequiredVars}} + ); + {{/parent}} + {{#vars}} + {{#required}} + this.{{name}} = {{name}}; + {{/required}} + {{/vars}} + } + + {{/hasRequired}} + {{/generateJsonCreator}} + {{/jackson}} + {{#vars}} + /** + {{#description}} + * {{.}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + **/ + public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + + {{#withXml}} + @XmlElement(name="{{baseName}}"{{#required}}, required = {{required}}{{/required}}) + {{/withXml}} + {{#vendorExtensions.x-extra-annotation}}{{{vendorExtensions.x-extra-annotation}}}{{/vendorExtensions.x-extra-annotation}}{{#useSwaggerAnnotations}} + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}"){{/useSwaggerAnnotations}}{{#useSwaggerV3Annotations}} + @Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}description = "{{{description}}}"){{/useSwaggerV3Annotations}}{{#useMicroProfileOpenAPIAnnotations}} + @org.eclipse.microprofile.openapi.annotations.media.Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}description = "{{{description}}}"){{/useMicroProfileOpenAPIAnnotations}} + {{#jackson}}@JsonProperty({{#required}}required = {{required}}, value = {{/required}}"{{baseName}}"){{/jackson}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}() { + return {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + {{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}}public {{>beanValidatedType}} {{getter}}() { + return {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{#jackson}} + @JsonProperty({{#required}}required = {{required}}, value = {{/required}}"{{baseName}}") + {{/jackson}} + {{#vendorExtensions.x-setter-extra-annotation}}{{{vendorExtensions.x-setter-extra-annotation}}} + {{/vendorExtensions.x-setter-extra-annotation}}public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + + {{#isArray}} + public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + + this.{{name}}.add({{name}}Item); + return this; + } + + public {{classname}} remove{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + if ({{name}}Item != null && this.{{name}} != null) { + this.{{name}}.remove({{name}}Item); + } + + return this; + } + {{/isArray}} + {{#isMap}} + public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + + this.{{name}}.put(key, {{name}}Item); + return this; + } + + public {{classname}} remove{{nameInPascalCase}}Item(String key) { + if (this.{{name}} != null) { + this.{{name}}.remove(key); + } + + return this; + } + {{/isMap}} + {{/vars}} +{{>additional_properties}} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + }{{#hasVars}} + {{classname}} {{classVarName}} = ({{classname}}) o; + return {{#vars}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} && + {{/-last}}{{/vars}}{{#parent}} && + super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} + return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} + } + + @Override + public int hashCode() { + return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}}sb.append(" ").append(toIndentedString(super.toString())).append("\n");{{/parent}} + {{#vars}}sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n"); + {{/vars}}sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +{{#generateBuilders}}{{^additionalProperties}} + public static {{classname}}Builder builder() { + return new {{classname}}BuilderImpl(); + } + + private static final class {{classname}}BuilderImpl extends {{classname}}Builder<{{classname}}, {{classname}}BuilderImpl> { + + @Override + protected {{classname}}BuilderImpl self() { + return this; + } + + @Override + public {{classname}} build() { + return new {{classname}}(this); + } + } + + public static abstract class {{classname}}Builder > {{#parent}}extends {{{.}}}Builder {{/parent}} { + {{#vars}} + private {{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/vars}} + {{^parent}} + protected abstract B self(); + + public abstract C build(); + {{/parent}} + + {{#vars}} + public B {{name}}({{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}) { + this.{{name}} = {{name}}; + return self(); + } + {{/vars}} + }{{/additionalProperties}}{{/generateBuilders}} +} diff --git a/src/main/resources/openapi-templates/additionalEnumTypeAnnotations.mustache b/src/main/resources/openapi-templates/additionalEnumTypeAnnotations.mustache new file mode 100644 index 0000000..452340a --- /dev/null +++ b/src/main/resources/openapi-templates/additionalEnumTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalEnumTypeAnnotations}}{{{.}}} +{{/additionalEnumTypeAnnotations}} diff --git a/src/main/resources/openapi-templates/additionalModelTypeAnnotations.mustache b/src/main/resources/openapi-templates/additionalModelTypeAnnotations.mustache new file mode 100644 index 0000000..a28cbba --- /dev/null +++ b/src/main/resources/openapi-templates/additionalModelTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalModelTypeAnnotations}}{{{.}}} +{{/additionalModelTypeAnnotations}} diff --git a/src/main/resources/openapi-templates/additional_properties.mustache b/src/main/resources/openapi-templates/additional_properties.mustache new file mode 100644 index 0000000..b9b0ff8 --- /dev/null +++ b/src/main/resources/openapi-templates/additional_properties.mustache @@ -0,0 +1,32 @@ +{{#additionalProperties}} + /** + * Set the additional (undeclared) property with the specified name and value. + * Creates the property if it does not already exist, otherwise replaces it. + * @param key the name of the property + * @param value the value of the property + * @return self reference + */ + @JsonAnySetter + public {{classname}} putAdditionalProperty(String key, {{{datatypeWithEnum}}} value) { + this.put(key, value); + return this; + } + + /** + * Return the additional (undeclared) properties. + * @return the additional (undeclared) properties + */ + @JsonAnyGetter + public Map getAdditionalProperties() { + return this; + } + + /** + * Return the additional (undeclared) property with the specified name. + * @param key the name of the property + * @return the additional (undeclared) property with the specified name + */ + public {{{datatypeWithEnum}}} getAdditionalProperty(String key) { + return this.get(key); + } +{{/additionalProperties}} diff --git a/src/main/resources/openapi-templates/beanValidatedType.mustache b/src/main/resources/openapi-templates/beanValidatedType.mustache new file mode 100644 index 0000000..45f432f --- /dev/null +++ b/src/main/resources/openapi-templates/beanValidatedType.mustache @@ -0,0 +1 @@ +{{#isArray}}{{baseType}}<{{#items}}{{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}}{{>beanValidatedType}}{{/items}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}} diff --git a/src/main/resources/openapi-templates/beanValidation.mustache b/src/main/resources/openapi-templates/beanValidation.mustache new file mode 100644 index 0000000..e639c3c --- /dev/null +++ b/src/main/resources/openapi-templates/beanValidation.mustache @@ -0,0 +1 @@ +{{#required}}{{^isReadOnly}}@NotNull {{/isReadOnly}}{{/required}}{{#isContainer}}{{! No @Valid on container (HV000271) }}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}{{^isDate}}{{^isDateTime}}{{^isString}}{{^isFile}}{{^isEnumOrRef}}@Valid {{/isEnumOrRef}}{{/isFile}}{{/isString}}{{/isDateTime}}{{/isDate}}{{/isPrimitiveType}}{{/isContainer}}{{>beanValidationCore}} diff --git a/src/main/resources/openapi-templates/beanValidationCore.mustache b/src/main/resources/openapi-templates/beanValidationCore.mustache new file mode 100644 index 0000000..2ce9be9 --- /dev/null +++ b/src/main/resources/openapi-templates/beanValidationCore.mustache @@ -0,0 +1,20 @@ +{{#pattern}} @Pattern(regexp="{{{.}}}"){{/pattern}}{{! +minLength && maxLength set +}}{{#minLength}}{{#maxLength}} @Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{! +minLength set, maxLength not +}}{{#minLength}}{{^maxLength}} @Size(min={{minLength}}){{/maxLength}}{{/minLength}}{{! +minLength not set, maxLength set +}}{{^minLength}}{{#maxLength}} @Size(max={{.}}){{/maxLength}}{{/minLength}}{{! +@Size: minItems && maxItems set +}}{{#minItems}}{{#maxItems}} @Size(min={{minItems}},max={{maxItems}}){{/maxItems}}{{/minItems}}{{! +@Size: minItems set, maxItems not +}}{{#minItems}}{{^maxItems}} @Size(min={{minItems}}){{/maxItems}}{{/minItems}}{{! +@Size: minItems not set && maxItems set +}}{{^minItems}}{{#maxItems}} @Size(max={{.}}){{/maxItems}}{{/minItems}}{{! +check for integer or long / all others=decimal type with @Decimal* +isInteger set +}}{{#isInteger}}{{#minimum}} @Min({{.}}){{/minimum}}{{#maximum}} @Max({{.}}){{/maximum}}{{/isInteger}}{{! +isLong set +}}{{#isLong}}{{#minimum}} @Min({{.}}L){{/minimum}}{{#maximum}} @Max({{.}}L){{/maximum}}{{/isLong}}{{! +Not Integer, not Long => we have a decimal value! +}}{{^isInteger}}{{^isLong}}{{#minimum}} @DecimalMin({{#exclusiveMinimum}}value={{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}},inclusive=false{{/exclusiveMinimum}}){{/minimum}}{{#maximum}} @DecimalMax({{#exclusiveMaximum}}value={{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}},inclusive=false{{/exclusiveMaximum}}){{/maximum}}{{/isLong}}{{/isInteger}} diff --git a/src/main/resources/openapi-templates/enumClass.mustache b/src/main/resources/openapi-templates/enumClass.mustache new file mode 100644 index 0000000..0dfb06d --- /dev/null +++ b/src/main/resources/openapi-templates/enumClass.mustache @@ -0,0 +1,56 @@ +{{#withXml}} +@XmlType(name="{{datatypeWithEnum}}") +@XmlEnum({{dataType}}.class) +{{/withXml}} +{{>additionalEnumTypeAnnotations}}public enum {{datatypeWithEnum}} { + + {{#allowableValues}} + {{#enumVars}}{{#withXml}}@XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}){{/withXml}}{{name}}({{dataType}}.valueOf({{{value}}})){{^-last}}, {{/-last}}{{#-last}};{{/-last}}{{/enumVars}} + {{/allowableValues}} + + + private {{dataType}} value; + + {{datatypeWithEnum}} ({{dataType}} v) { + value = v; + } + + public {{dataType}} value() { + return value; + } + + @Override + {{#jackson}} + @JsonValue + {{/jackson}} + public String toString() { + return String.valueOf(value); + } + + /** + * Convert a String into {{dataType}}, as specified in the + * See JAX RS 2.0 Specification, section 3.2, p. 12 + */ + public static {{datatypeWithEnum}} fromString(String s) { + for ({{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + // using Objects.toString() to be safe if value type non-object type + // because types like 'int' etc. will be auto-boxed + if (java.util.Objects.toString(b.value).equals(s)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected string value '" + s + "'");{{/isNullable}} + } + + {{#jackson}} + @JsonCreator + public static {{datatypeWithEnum}} fromValue({{dataType}} value) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (b.value.{{^isString}}equals{{/isString}}{{#isString}}{{#useEnumCaseInsensitive}}equalsIgnoreCase{{/useEnumCaseInsensitive}}{{^useEnumCaseInsensitive}}equals{{/useEnumCaseInsensitive}}{{/isString}}(value)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}{{#enumUnknownDefaultCase}}{{#allowableValues}}{{#enumVars}}{{#-last}}return {{{name}}};{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/enumUnknownDefaultCase}}{{/isNullable}} + } + {{/jackson}} +} diff --git a/src/main/resources/openapi-templates/enumOuterClass.mustache b/src/main/resources/openapi-templates/enumOuterClass.mustache new file mode 100644 index 0000000..9c89c21 --- /dev/null +++ b/src/main/resources/openapi-templates/enumOuterClass.mustache @@ -0,0 +1,64 @@ +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +{{/jackson}} + +/** + * {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} + */ +{{>generatedAnnotation}} + +{{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} { + {{#gson}} + {{#allowableValues}}{{#enumVars}} + @SerializedName({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}} + {{/gson}} + {{^gson}} + {{#allowableValues}}{{#enumVars}} + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}} + {{/gson}} + + private {{{dataType}}} value; + + {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}({{{dataType}}} value) { + this.value = value; + } + + /** + * Convert a String into {{dataType}}, as specified in the + * See JAX RS 2.0 Specification, section 3.2, p. 12 + */ + public static {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromString(String s) { + for ({{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + // using Objects.toString() to be safe if value type non-object type + // because types like 'int' etc. will be auto-boxed + if (java.util.Objects.toString(b.value).equals(s)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}throw new IllegalArgumentException("Unexpected string value '" + s + "'");{{/isNullable}} + } + + @Override + {{#jackson}} + @JsonValue + {{/jackson}} + public String toString() { + return String.valueOf(value); + } + + {{#jackson}} + @JsonCreator + public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (b.value.{{^isString}}equals{{/isString}}{{#isString}}{{#useEnumCaseInsensitive}}equalsIgnoreCase{{/useEnumCaseInsensitive}}{{^useEnumCaseInsensitive}}equals{{/useEnumCaseInsensitive}}{{/isString}}(value)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}{{#enumUnknownDefaultCase}}{{#allowableValues}}{{#enumVars}}{{#-last}}return {{{name}}};{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/enumUnknownDefaultCase}}{{/isNullable}} + } + {{/jackson}} +} diff --git a/src/main/resources/openapi-templates/generatedAnnotation.mustache b/src/main/resources/openapi-templates/generatedAnnotation.mustache new file mode 100644 index 0000000..fdd494a --- /dev/null +++ b/src/main/resources/openapi-templates/generatedAnnotation.mustache @@ -0,0 +1 @@ +@{{javaxPackage}}.annotation.Generated(value = "{{generatorClass}}"{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}}, comments = "Generator version: {{generatorVersion}}") diff --git a/src/main/resources/openapi-templates/model.mustache b/src/main/resources/openapi-templates/model.mustache new file mode 100644 index 0000000..03d5488 --- /dev/null +++ b/src/main/resources/openapi-templates/model.mustache @@ -0,0 +1,21 @@ +package {{package}}; + +{{#imports}}import {{import}}; +{{/imports}} +{{#serializableModel}} +import java.io.Serializable; +{{/serializableModel}} +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; +{{/useBeanValidation}} + +{{#models}} +{{#model}} +{{#isEnum}} +{{>enumOuterClass}} + +{{/isEnum}} +{{^isEnum}}{{>pojo}}{{/isEnum}} +{{/model}} +{{/models}} diff --git a/src/main/resources/openapi-templates/pojo.mustache b/src/main/resources/openapi-templates/pojo.mustache new file mode 100644 index 0000000..04480c4 --- /dev/null +++ b/src/main/resources/openapi-templates/pojo.mustache @@ -0,0 +1,289 @@ +{{#useSwaggerAnnotations}} +import io.swagger.annotations.*; +{{/useSwaggerAnnotations}} +{{#useSwaggerV3Annotations}} +import io.swagger.v3.oas.annotations.media.Schema; +{{/useSwaggerV3Annotations}} +import java.util.Objects; +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeName; +{{#additionalProperties}} +import java.util.Map; +import java.util.HashMap; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonFormat; +{{/additionalProperties}} +{{/jackson}} +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullable; +{{/openApiNullable}} +{{#withXml}} +import {{javaxPackage}}.xml.bind.annotation.XmlElement; +import {{javaxPackage}}.xml.bind.annotation.XmlRootElement; +import {{javaxPackage}}.xml.bind.annotation.XmlAccessType; +import {{javaxPackage}}.xml.bind.annotation.XmlAccessorType; +import {{javaxPackage}}.xml.bind.annotation.XmlType; +import {{javaxPackage}}.xml.bind.annotation.XmlEnum; +import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue; +{{/withXml}} + +{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{#description}}/** + * {{.}} + **/{{/description}} +{{#useSwaggerAnnotations}}{{#description}}@ApiModel(description = "{{{.}}}"){{/description}}{{/useSwaggerAnnotations}}{{#useSwaggerV3Annotations}} +@Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}){{/useSwaggerV3Annotations}}{{#useMicroProfileOpenAPIAnnotations}} +@org.eclipse.microprofile.openapi.annotations.media.Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}){{/useMicroProfileOpenAPIAnnotations}} +{{#jackson}} +@JsonTypeName("{{name}}") +{{#additionalProperties}} +@JsonFormat(shape=JsonFormat.Shape.OBJECT) +{{/additionalProperties}} +{{/jackson}} +{{>generatedAnnotation}}{{>additionalModelTypeAnnotations}}{{>xmlPojoAnnotation}} +{{#vendorExtensions.x-class-extra-annotation}} +{{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}} +public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{#vars}} + {{#isEnum}} + {{^isContainer}} + {{>enumClass}}{{! prevent indent}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} + {{>enumClass}}{{! prevent indent}} + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#vendorExtensions.x-field-extra-annotation}} + {{{.}}} + {{/vendorExtensions.x-field-extra-annotation}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); + {{/isContainer}} + {{^isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + {{/isContainer}} + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + private {{#useBeanValidation}}{{>beanValidatedType}}{{/useBeanValidation}}{{^useBeanValidation}}{{{datatypeWithEnum}}}{{/useBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{/vars}} + + {{#generateBuilders}} + {{^additionalProperties}} + protected {{classname}}({{classname}}Builder b) { + {{#parent}} + super(b); + {{/parent}} + {{#vars}} + this.{{name}} = b.{{name}}; + {{/vars}} + } + + {{/additionalProperties}} + {{/generateBuilders}} + public {{classname}}() { + } + + {{#jackson}} + {{#generateJsonCreator}} + {{#hasRequired}} + @JsonCreator + public {{classname}}( + {{#requiredVars}} + @JsonProperty(required = {{required}}, value = "{{baseName}}") {{>beanValidatedType}} {{name}}{{^-last}},{{/-last}} + {{/requiredVars}} + ) { + {{#parent}} + super( + {{#parentRequiredVars}} + {{name}}{{^-last}},{{/-last}} + {{/parentRequiredVars}} + ); + {{/parent}} + {{#vars}} + {{#required}} + this.{{name}} = {{name}}; + {{/required}} + {{/vars}} + } + + {{/hasRequired}} + {{/generateJsonCreator}} + {{/jackson}} + {{#vars}} + /** + {{#description}} + * {{.}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + **/ + public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + + {{#withXml}} + @XmlElement(name="{{baseName}}"{{#required}}, required = {{required}}{{/required}}) + {{/withXml}} + {{#vendorExtensions.x-extra-annotation}}{{{vendorExtensions.x-extra-annotation}}}{{/vendorExtensions.x-extra-annotation}}{{#useSwaggerAnnotations}} + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}"){{/useSwaggerAnnotations}}{{#useSwaggerV3Annotations}} + @Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}description = "{{{description}}}"){{/useSwaggerV3Annotations}}{{#useMicroProfileOpenAPIAnnotations}} + @org.eclipse.microprofile.openapi.annotations.media.Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}description = "{{{description}}}"){{/useMicroProfileOpenAPIAnnotations}} + {{#jackson}}@JsonProperty({{#required}}required = {{required}}, value = {{/required}}"{{baseName}}"){{/jackson}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}() { + return {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + {{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}}public {{>beanValidatedType}} {{getter}}() { + return {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{#jackson}} + @JsonProperty({{#required}}required = {{required}}, value = {{/required}}"{{baseName}}") + {{/jackson}} + {{#vendorExtensions.x-setter-extra-annotation}}{{{vendorExtensions.x-setter-extra-annotation}}} + {{/vendorExtensions.x-setter-extra-annotation}}public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + + {{#isArray}} + public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + + this.{{name}}.add({{name}}Item); + return this; + } + + public {{classname}} remove{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + if ({{name}}Item != null && this.{{name}} != null) { + this.{{name}}.remove({{name}}Item); + } + + return this; + } + {{/isArray}} + {{#isMap}} + public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + + this.{{name}}.put(key, {{name}}Item); + return this; + } + + public {{classname}} remove{{nameInPascalCase}}Item(String key) { + if (this.{{name}} != null) { + this.{{name}}.remove(key); + } + + return this; + } + {{/isMap}} + {{/vars}} +{{>additional_properties}} + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + }{{#hasVars}} + {{classname}} {{classVarName}} = ({{classname}}) o; + return {{#vars}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} && + {{/-last}}{{/vars}}{{#parent}} && + super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} + return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} + } + + @Override + public int hashCode() { + return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}}sb.append(" ").append(toIndentedString(super.toString())).append("\n");{{/parent}} + {{#vars}}sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n"); + {{/vars}}sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +{{#generateBuilders}}{{^additionalProperties}} + public static {{classname}}Builder builder() { + return new {{classname}}BuilderImpl(); + } + + private static final class {{classname}}BuilderImpl extends {{classname}}Builder<{{classname}}, {{classname}}BuilderImpl> { + + @Override + protected {{classname}}BuilderImpl self() { + return this; + } + + @Override + public {{classname}} build() { + return new {{classname}}(this); + } + } + + public static abstract class {{classname}}Builder > {{#parent}}extends {{{.}}}Builder {{/parent}} { + {{#vars}} + private {{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/vars}} + {{^parent}} + protected abstract B self(); + + public abstract C build(); + {{/parent}} + + {{#vars}} + public B {{name}}({{#removeAnnotations}}{{{datatypeWithEnum}}}{{/removeAnnotations}} {{name}}) { + this.{{name}} = {{name}}; + return self(); + } + {{/vars}} + }{{/additionalProperties}}{{/generateBuilders}} +} diff --git a/src/main/resources/openapi-templates/typeInfoAnnotation.mustache b/src/main/resources/openapi-templates/typeInfoAnnotation.mustache new file mode 100644 index 0000000..cf69aa9 --- /dev/null +++ b/src/main/resources/openapi-templates/typeInfoAnnotation.mustache @@ -0,0 +1,8 @@ +{{#jackson}} +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) +@JsonSubTypes({ + {{#discriminator.mappedModels}} + @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"), + {{/discriminator.mappedModels}} +}) +{{/jackson}} diff --git a/src/main/resources/openapi-templates/xmlPojoAnnotation.mustache b/src/main/resources/openapi-templates/xmlPojoAnnotation.mustache new file mode 100644 index 0000000..03ec284 --- /dev/null +++ b/src/main/resources/openapi-templates/xmlPojoAnnotation.mustache @@ -0,0 +1,8 @@ +{{#withXml}} + @XmlAccessorType(XmlAccessType.FIELD) + {{#hasVars}} @XmlType(name = "{{classname}}", propOrder = + { {{#vars}}"{{name}}"{{^-last}}, {{/-last}}{{/vars}} + }){{/hasVars}} + {{^hasVars}}@XmlType(name = "{{classname}}"){{/hasVars}} + {{^parent}}@XmlRootElement(name="{{classname}}"){{/parent}} +{{/withXml}} diff --git a/src/main/web/src/pages/StartPage.tsx b/src/main/web/src/pages/StartPage.tsx index a92b818..9e52846 100644 --- a/src/main/web/src/pages/StartPage.tsx +++ b/src/main/web/src/pages/StartPage.tsx @@ -36,11 +36,13 @@ import type { CreateSessionRequest, ModelInfo, ScenarioSetup, + ScenarioSummary, } from '../api/generated/index' import { Configuration, CreateSessionRequestSafetyLevelEnum, ModelsApi, + ScenariosApi, SessionsApi, } from '../api/generated/index' @@ -49,6 +51,7 @@ const API_BASE = '/api/v1' const modelsApi = new ModelsApi(new Configuration({basePath: API_BASE})) const sessionsApi = new SessionsApi(new Configuration({basePath: API_BASE})) +const scenariosApi = new ScenariosApi(new Configuration({basePath: API_BASE})) /** * Landing page where the user selects an Ollama model and optional settings @@ -64,6 +67,9 @@ export default function StartPage() { const [starting, setStarting] = useState(false) const [error, setError] = useState(null) + /** Saved scenarios from API; selectedScenarioId is '' for custom or a UUID. */ + const [scenarios, setScenarios] = useState([]) + const [selectedScenarioId, setSelectedScenarioId] = useState('') /** Optional scenario (collapsible). */ const [scenarioExpanded, setScenarioExpanded] = useState(false) const [setting, setSetting] = useState('') @@ -77,14 +83,18 @@ export default function StartPage() { draft: CharacterDefinition } | null>(null) - /** Load available models on mount. */ + /** Load available models and saved scenarios on mount. */ useEffect(() => { - modelsApi.listModels() - .then((resp) => { - setModels(resp.models ?? []) - if (resp.models && resp.models.length > 0) { - setSelectedModel(resp.models[0].name) + Promise.all([ + modelsApi.listModels(), + scenariosApi.listScenarios().catch(() => ({scenarios: []})), + ]) + .then(([modelsResp, scenariosResp]) => { + setModels(modelsResp.models ?? []) + if (modelsResp.models && modelsResp.models.length > 0) { + setSelectedModel(modelsResp.models[0].name) } + setScenarios(scenariosResp.scenarios ?? []) }) .catch(() => setError('Could not load models. Is the Quarkus server running?')) .finally(() => setLoading(false)) @@ -94,6 +104,22 @@ export default function StartPage() { setSelectedModel(event.target.value) } + /** When user selects a saved scenario, load its full setup and prefill the form. */ + const handleScenarioSelect = async (event: SelectChangeEvent) => { + const id = event.target.value + setSelectedScenarioId(id) + if (!id) return + try { + const setup = await scenariosApi.getScenario({scenarioId: id}) + setSetting(setup.setting ?? '') + setInitialConflict(setup.initialConflict ?? '') + setUserCharacter(setup.userCharacter ?? null) + setAiCharacters(setup.aiCharacters ?? []) + } catch { + setError('Could not load scenario.') + } + } + /** Build scenario from form state if any field is filled. */ const buildScenario = (): ScenarioSetup | undefined => { const hasSetting = setting.trim() !== '' @@ -166,7 +192,8 @@ export default function StartPage() { model: selectedModel, language, safetyLevel: CreateSessionRequestSafetyLevelEnum.standard, - scenario: buildScenario(), + scenarioId: selectedScenarioId || undefined, + scenario: selectedScenarioId ? undefined : buildScenario(), } const session = await sessionsApi.createSession({createSessionRequest: request}) navigate(`/session/${session.sessionId}`) @@ -229,6 +256,25 @@ export default function StartPage() { fullWidth /> + + Saved scenario + + + setScenarioExpanded((b) => !b)} diff --git a/src/test/java/de/neitzel/roleplay/business/InMemorySessionServiceTest.java b/src/test/java/de/neitzel/roleplay/business/InMemorySessionServiceTest.java index 0e6c335..abcbe12 100644 --- a/src/test/java/de/neitzel/roleplay/business/InMemorySessionServiceTest.java +++ b/src/test/java/de/neitzel/roleplay/business/InMemorySessionServiceTest.java @@ -21,6 +21,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -39,18 +40,21 @@ class InMemorySessionServiceTest { @Mock private OllamaClient ollamaClient; + @Mock + private ScenarioService scenarioService; + private final ObjectMapper objectMapper = new ObjectMapper(); private InMemorySessionService sessionService; /** - * Creates a fresh service instance with mocked Ollama client before each test. + * Creates a fresh service instance with mocked Ollama client and scenario service before each test. * By default, Ollama is stubbed to return a short narrative and an empty * state update so that createSession (with scenario) and submitTurn complete. */ @BeforeEach void setUp() { - sessionService = new InMemorySessionService(ollamaClient, objectMapper); + sessionService = new InMemorySessionService(ollamaClient, objectMapper, scenarioService); StateUpdateResponse emptyUpdate = StateUpdateResponse.builder() .responses(null) .updatedSituation(null) @@ -238,6 +242,42 @@ class InMemorySessionServiceTest { assertEquals("Old Sage", aiState.getName()); } + /** + * Verifies that creating a session with scenarioId uses the scenario loaded from the database. + */ + @Test + void createSessionWithScenarioIdUsesScenarioFromDatabase() { + UUID scenarioId = UUID.fromString("22222222-2222-2222-2222-222222222201"); + CharacterDefinition userChar = new CharacterDefinition("db_user", "DB User", "protagonist"); + CharacterDefinition aiChar = new CharacterDefinition("db_ai", "DB NPC", "antagonist"); + ScenarioSetup loadedScenario = new ScenarioSetup(); + loadedScenario.setSetting("Database setting"); + loadedScenario.setInitialConflict("Database conflict"); + loadedScenario.setUserCharacter(userChar); + loadedScenario.setAiCharacters(List.of(aiChar)); + when(scenarioService.getScenarioAsSetup(scenarioId)).thenReturn(Optional.of(loadedScenario)); + + CreateSessionRequest request = new CreateSessionRequest("llama3:latest"); + request.setScenarioId(scenarioId); + request.setScenario(null); + + SessionResponse response = sessionService.createSession(request); + + assertNotNull(response.getSituation()); + assertEquals("Database setting", response.getSituation().getSetting()); + assertNotNull(response.getCharacters()); + assertEquals(2, response.getCharacters().size()); + CharacterState userState = response.getCharacters().stream() + .filter(c -> Boolean.TRUE.equals(c.getIsUserCharacter())) + .findFirst().orElseThrow(); + assertEquals("db_user", userState.getId()); + assertEquals("DB User", userState.getName()); + CharacterState aiState = response.getCharacters().stream() + .filter(c -> Boolean.FALSE.equals(c.getIsUserCharacter())) + .findFirst().orElseThrow(); + assertEquals("db_ai", aiState.getId()); + } + /** * Verifies that updateSession updates situation and characters when provided. */ diff --git a/src/test/java/de/neitzel/roleplay/business/ScenarioServiceTest.java b/src/test/java/de/neitzel/roleplay/business/ScenarioServiceTest.java new file mode 100644 index 0000000..e68b6ab --- /dev/null +++ b/src/test/java/de/neitzel/roleplay/business/ScenarioServiceTest.java @@ -0,0 +1,127 @@ +package de.neitzel.roleplay.business; + +import de.neitzel.roleplay.data.CharacterEntity; +import de.neitzel.roleplay.data.ScenarioCharacterEntity; +import de.neitzel.roleplay.data.ScenarioEntity; +import de.neitzel.roleplay.data.ScenarioRepository; +import de.neitzel.roleplay.fascade.model.ScenarioSetup; +import de.neitzel.roleplay.fascade.model.ScenarioSummary; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link ScenarioService}. + */ +@ExtendWith(MockitoExtension.class) +class ScenarioServiceTest { + + @Mock + private ScenarioRepository scenarioRepository; + + private ScenarioService scenarioService; + + @BeforeEach + void setUp() { + scenarioService = new ScenarioService(scenarioRepository); + } + + @Test + void listScenariosReturnsMappedSummaries() { + ScenarioEntity entity = new ScenarioEntity(); + entity.setId(UUID.fromString("22222222-2222-2222-2222-222222222201")); + entity.setName("Harbour mystery"); + entity.setSetting("A fog-covered harbour"); + entity.setInitialConflict("Strange noises"); + when(scenarioRepository.listAll()).thenReturn(List.of(entity)); + + List result = scenarioService.listScenarios(); + + assertEquals(1, result.size()); + ScenarioSummary summary = result.get(0); + assertEquals(UUID.fromString("22222222-2222-2222-2222-222222222201"), summary.getId()); + assertEquals("Harbour mystery", summary.getName()); + assertEquals("A fog-covered harbour", summary.getSetting()); + assertEquals("Strange noises", summary.getInitialConflict()); + } + + @Test + void listScenariosReturnsEmptyWhenNoneStored() { + when(scenarioRepository.listAll()).thenReturn(List.of()); + + List result = scenarioService.listScenarios(); + + assertTrue(result.isEmpty()); + } + + @Test + void getScenarioAsSetupReturnsEmptyWhenNotFound() { + UUID id = UUID.randomUUID(); + when(scenarioRepository.findByIdWithCharacters(id)).thenReturn(null); + + Optional result = scenarioService.getScenarioAsSetup(id); + + assertTrue(result.isEmpty()); + } + + @Test + void getScenarioAsSetupReturnsSetupWithUserAndAiCharacters() { + UUID scenarioId = UUID.fromString("22222222-2222-2222-2222-222222222201"); + UUID userCharId = UUID.fromString("11111111-1111-1111-1111-111111111101"); + UUID aiCharId = UUID.fromString("11111111-1111-1111-1111-111111111102"); + + CharacterEntity userCharEntity = new CharacterEntity(); + userCharEntity.setId(userCharId); + userCharEntity.setName("The Detective"); + userCharEntity.setRole("detective"); + + CharacterEntity aiCharEntity = new CharacterEntity(); + aiCharEntity.setId(aiCharId); + aiCharEntity.setName("Captain Morgan"); + aiCharEntity.setRole("captain"); + + ScenarioCharacterEntity userLink = new ScenarioCharacterEntity(); + userLink.setUserCharacter(true); + userLink.setPosition(0); + userLink.setCharacter(userCharEntity); + + ScenarioCharacterEntity aiLink = new ScenarioCharacterEntity(); + aiLink.setUserCharacter(false); + aiLink.setPosition(1); + aiLink.setCharacter(aiCharEntity); + + ScenarioEntity scenarioEntity = new ScenarioEntity(); + scenarioEntity.setId(scenarioId); + scenarioEntity.setName("Harbour mystery"); + scenarioEntity.setSetting("A fog-covered harbour at dawn"); + scenarioEntity.setInitialConflict("Strange noises from the cargo hold"); + scenarioEntity.setScenarioCharacters(List.of(userLink, aiLink)); + + when(scenarioRepository.findByIdWithCharacters(scenarioId)).thenReturn(scenarioEntity); + + Optional result = scenarioService.getScenarioAsSetup(scenarioId); + + assertTrue(result.isPresent()); + ScenarioSetup setup = result.get(); + assertEquals("A fog-covered harbour at dawn", setup.getSetting()); + assertEquals("Strange noises from the cargo hold", setup.getInitialConflict()); + assertNotNull(setup.getUserCharacter()); + assertEquals("The Detective", setup.getUserCharacter().getName()); + assertEquals("detective", setup.getUserCharacter().getRole()); + assertNotNull(setup.getAiCharacters()); + assertEquals(1, setup.getAiCharacters().size()); + assertEquals("Captain Morgan", setup.getAiCharacters().get(0).getName()); + assertEquals("captain", setup.getAiCharacters().get(0).getRole()); + } +} diff --git a/tree.txt b/tree.txt new file mode 100644 index 0000000..59e4b08 --- /dev/null +++ b/tree.txt @@ -0,0 +1,216 @@ +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------< de.neitzel:roleplay >------------------------- +[INFO] Building roleplay 0.1.0-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] --- dependency:3.7.0:tree (default-cli) @ roleplay --- +[INFO] de.neitzel:roleplay:jar:0.1.0-SNAPSHOT +[INFO] +- io.quarkus:quarkus-arc:jar:3.31.1:compile +[INFO] | +- io.quarkus.arc:arc:jar:3.31.1:compile +[INFO] | | +- jakarta.enterprise:jakarta.enterprise.cdi-api:jar:4.1.0:compile +[INFO] | | | +- jakarta.enterprise:jakarta.enterprise.lang-model:jar:4.1.0:compile +[INFO] | | | \- jakarta.interceptor:jakarta.interceptor-api:jar:2.2.0:compile +[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:3.0.0:compile +[INFO] | | +- jakarta.transaction:jakarta.transaction-api:jar:2.0.1:compile +[INFO] | | +- io.smallrye.reactive:mutiny:jar:3.1.0:compile +[INFO] | | | +- io.smallrye.common:smallrye-common-annotation:jar:2.15.0:compile +[INFO] | | | \- org.jctools:jctools-core:jar:4.0.5:compile +[INFO] | | \- org.jboss.logging:jboss-logging:jar:3.6.2.Final:compile +[INFO] | +- io.quarkus:quarkus-core:jar:3.31.1:compile +[INFO] | | +- jakarta.inject:jakarta.inject-api:jar:2.0.1:compile +[INFO] | | +- io.smallrye.common:smallrye-common-os:jar:2.15.0:compile +[INFO] | | +- io.quarkus:quarkus-ide-launcher:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-development-mode-spi:jar:3.31.1:compile +[INFO] | | +- io.smallrye.config:smallrye-config:jar:3.15.1:compile +[INFO] | | | \- io.smallrye.config:smallrye-config-core:jar:3.15.1:compile +[INFO] | | +- org.jboss.logmanager:jboss-logmanager:jar:3.2.0.Final:compile +[INFO] | | | +- io.smallrye.common:smallrye-common-cpu:jar:2.15.0:compile +[INFO] | | | +- io.smallrye.common:smallrye-common-expression:jar:2.15.0:compile +[INFO] | | | +- io.smallrye.common:smallrye-common-net:jar:2.15.0:compile +[INFO] | | | +- io.smallrye.common:smallrye-common-ref:jar:2.15.0:compile +[INFO] | | | \- jakarta.json:jakarta.json-api:jar:2.1.3:compile +[INFO] | | +- org.jboss.threads:jboss-threads:jar:3.9.2:compile +[INFO] | | | \- io.smallrye.common:smallrye-common-function:jar:2.15.0:compile +[INFO] | | +- org.slf4j:slf4j-api:jar:2.0.17:compile +[INFO] | | +- org.jboss.slf4j:slf4j-jboss-logmanager:jar:2.0.2.Final:compile +[INFO] | | +- org.wildfly.common:wildfly-common:jar:2.0.1:compile +[INFO] | | +- io.quarkus:quarkus-registry:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-bootstrap-runner:jar:3.31.1:compile +[INFO] | | | +- io.quarkus:quarkus-classloader-commons:jar:3.31.1:compile +[INFO] | | | \- io.smallrye.common:smallrye-common-io:jar:2.15.0:compile +[INFO] | | \- io.quarkus:quarkus-fs-util:jar:1.3.0:compile +[INFO] | \- org.eclipse.microprofile.context-propagation:microprofile-context-propagation-api:jar:1.3:compile +[INFO] +- io.quarkus:quarkus-rest:jar:3.31.1:compile +[INFO] | +- io.quarkus:quarkus-rest-common:jar:3.31.1:compile +[INFO] | | +- io.quarkus.resteasy.reactive:resteasy-reactive-common:jar:3.31.1:compile +[INFO] | | | +- io.quarkus.resteasy.reactive:resteasy-reactive-common-types:jar:3.31.1:compile +[INFO] | | | \- org.reactivestreams:reactive-streams:jar:1.0.4:compile +[INFO] | | +- io.quarkus:quarkus-mutiny:jar:3.31.1:compile +[INFO] | | | +- io.quarkus:quarkus-smallrye-context-propagation:jar:3.31.1:compile +[INFO] | | | | \- io.smallrye:smallrye-context-propagation:jar:2.3.0:compile +[INFO] | | | | +- io.smallrye:smallrye-context-propagation-api:jar:2.3.0:compile +[INFO] | | | | \- io.smallrye:smallrye-context-propagation-storage:jar:2.3.0:compile +[INFO] | | | \- io.smallrye.reactive:mutiny-smallrye-context-propagation:jar:3.1.0:compile +[INFO] | | \- io.quarkus:quarkus-vertx:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-netty:jar:3.31.1:compile +[INFO] | | | \- io.netty:netty-codec:jar:4.1.130.Final:compile +[INFO] | | +- io.netty:netty-codec-haproxy:jar:4.1.130.Final:compile +[INFO] | | +- io.quarkus:quarkus-vertx-latebound-mdc-provider:jar:3.31.1:compile +[INFO] | | \- io.smallrye:smallrye-fault-tolerance-vertx:jar:6.10.0:compile +[INFO] | +- io.quarkus.resteasy.reactive:resteasy-reactive-vertx:jar:3.31.1:compile +[INFO] | | +- io.vertx:vertx-web:jar:4.5.24:compile +[INFO] | | | +- io.vertx:vertx-web-common:jar:4.5.24:compile +[INFO] | | | +- io.vertx:vertx-auth-common:jar:4.5.24:compile +[INFO] | | | \- io.vertx:vertx-bridge-common:jar:4.5.24:compile +[INFO] | | +- io.smallrye.reactive:smallrye-mutiny-vertx-core:jar:3.21.4:compile +[INFO] | | | +- io.smallrye.reactive:smallrye-mutiny-vertx-runtime:jar:3.21.4:compile +[INFO] | | | \- io.smallrye.reactive:vertx-mutiny-generator:jar:3.21.4:compile +[INFO] | | | \- io.vertx:vertx-codegen:jar:4.5.24:compile +[INFO] | | +- io.quarkus.resteasy.reactive:resteasy-reactive:jar:3.31.1:compile +[INFO] | | +- io.quarkus.vertx.utils:quarkus-vertx-utils:jar:3.31.1:compile +[INFO] | | +- org.jboss.logging:commons-logging-jboss-logging:jar:1.0.0.Final:compile +[INFO] | | \- jakarta.xml.bind:jakarta.xml.bind-api:jar:4.0.4:compile +[INFO] | | \- jakarta.activation:jakarta.activation-api:jar:2.1.4:compile +[INFO] | +- io.quarkus:quarkus-vertx-http:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-security-runtime-spi:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-tls-registry:jar:3.31.1:compile +[INFO] | | | +- io.quarkus:quarkus-tls-registry-spi:jar:3.31.1:compile +[INFO] | | | \- io.smallrye.certs:smallrye-private-key-pem-parser:jar:0.9.2:compile +[INFO] | | +- io.quarkus:quarkus-credentials:jar:3.31.1:compile +[INFO] | | +- io.smallrye.common:smallrye-common-vertx-context:jar:2.15.0:compile +[INFO] | | +- io.quarkus.security:quarkus-security:jar:2.3.2:compile +[INFO] | | +- io.smallrye.reactive:smallrye-mutiny-vertx-web:jar:3.21.4:compile +[INFO] | | | +- io.smallrye.reactive:smallrye-mutiny-vertx-web-common:jar:3.21.4:compile +[INFO] | | | +- io.smallrye.reactive:smallrye-mutiny-vertx-auth-common:jar:3.21.4:compile +[INFO] | | | +- io.smallrye.reactive:smallrye-mutiny-vertx-bridge-common:jar:3.21.4:compile +[INFO] | | | \- io.smallrye.reactive:smallrye-mutiny-vertx-uri-template:jar:3.21.4:compile +[INFO] | | | \- io.vertx:vertx-uri-template:jar:4.5.24:compile +[INFO] | | +- org.crac:crac:jar:1.5.0:compile +[INFO] | | \- com.aayushatharva.brotli4j:brotli4j:jar:1.16.0:compile +[INFO] | | +- com.aayushatharva.brotli4j:service:jar:1.16.0:compile +[INFO] | | \- com.aayushatharva.brotli4j:native-osx-aarch64:jar:1.16.0:compile +[INFO] | +- io.quarkus:quarkus-jsonp:jar:3.31.1:compile +[INFO] | | \- org.eclipse.parsson:parsson:jar:1.1.7:compile +[INFO] | \- io.quarkus:quarkus-virtual-threads:jar:3.31.1:compile +[INFO] | \- io.vertx:vertx-core:jar:4.5.24:compile +[INFO] | +- io.netty:netty-common:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-buffer:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-transport:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-handler:jar:4.1.130.Final:compile +[INFO] | | \- io.netty:netty-transport-native-unix-common:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-handler-proxy:jar:4.1.130.Final:compile +[INFO] | | \- io.netty:netty-codec-socks:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-codec-http:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-codec-http2:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-resolver:jar:4.1.130.Final:compile +[INFO] | +- io.netty:netty-resolver-dns:jar:4.1.130.Final:compile +[INFO] | | \- io.netty:netty-codec-dns:jar:4.1.130.Final:compile +[INFO] | \- com.fasterxml.jackson.core:jackson-core:jar:2.20.1:compile +[INFO] +- io.quarkus:quarkus-rest-jackson:jar:3.31.1:compile +[INFO] | \- io.quarkus:quarkus-rest-jackson-common:jar:3.31.1:compile +[INFO] | \- io.quarkus:quarkus-jackson:jar:3.31.1:compile +[INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.20.1:compile +[INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.20.1:compile +[INFO] | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.20.1:compile +[INFO] +- io.quarkus:quarkus-hibernate-validator:jar:3.31.1:compile +[INFO] | +- org.hibernate.validator:hibernate-validator:jar:9.1.0.Final:compile +[INFO] | | +- jakarta.validation:jakarta.validation-api:jar:3.1.1:compile +[INFO] | | \- com.fasterxml:classmate:jar:1.7.1:compile +[INFO] | +- org.glassfish.expressly:expressly:jar:6.0.0:compile +[INFO] | | \- jakarta.el:jakarta.el-api:jar:6.0.1:compile +[INFO] | +- io.smallrye.config:smallrye-config-validator:jar:3.15.1:compile +[INFO] | \- jakarta.ws.rs:jakarta.ws.rs-api:jar:3.1.0:compile +[INFO] +- io.quarkus:quarkus-config-yaml:jar:3.31.1:compile +[INFO] | +- io.smallrye.config:smallrye-config-source-yaml:jar:3.15.1:compile +[INFO] | | +- io.smallrye.config:smallrye-config-common:jar:3.15.1:compile +[INFO] | | | \- io.smallrye.common:smallrye-common-classloader:jar:2.15.0:compile +[INFO] | | \- io.smallrye.common:smallrye-common-constraint:jar:2.15.0:compile +[INFO] | \- org.eclipse.microprofile.config:microprofile-config-api:jar:3.1:compile +[INFO] +- io.quarkus:quarkus-jdbc-h2:jar:3.31.1:compile +[INFO] | +- com.h2database:h2:jar:2.4.240:compile +[INFO] | \- org.locationtech.jts:jts-core:jar:1.19.0:compile +[INFO] +- io.quarkus:quarkus-liquibase:jar:3.31.1:compile +[INFO] | +- io.quarkus:quarkus-liquibase-common:jar:3.31.1:compile +[INFO] | +- org.liquibase:liquibase-core:jar:4.33.0:compile +[INFO] | | +- com.opencsv:opencsv:jar:5.11.2:compile +[INFO] | | +- org.apache.commons:commons-collections4:jar:4.5.0:compile +[INFO] | | +- org.apache.commons:commons-text:jar:1.15.0:compile +[INFO] | | +- org.apache.commons:commons-lang3:jar:3.20.0:compile +[INFO] | | \- commons-io:commons-io:jar:2.21.0:compile +[INFO] | +- org.osgi:osgi.core:jar:6.0.0:compile +[INFO] | +- org.yaml:snakeyaml:jar:2.5:compile +[INFO] | +- io.quarkus:quarkus-jaxb:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-jaxp:jar:3.31.1:compile +[INFO] | | \- org.glassfish.jaxb:jaxb-runtime:jar:4.0.6:compile +[INFO] | | \- org.glassfish.jaxb:jaxb-core:jar:4.0.6:compile +[INFO] | | +- org.eclipse.angus:angus-activation:jar:2.0.3:runtime +[INFO] | | +- org.glassfish.jaxb:txw2:jar:4.0.6:compile +[INFO] | | \- com.sun.istack:istack-commons-runtime:jar:4.1.2:compile +[INFO] | +- io.quarkus:quarkus-agroal:jar:3.31.1:compile +[INFO] | | +- io.quarkus:quarkus-datasource:jar:3.31.1:compile +[INFO] | | +- io.agroal:agroal-api:jar:2.8:compile +[INFO] | | +- io.agroal:agroal-narayana:jar:2.8:compile +[INFO] | | | \- org.jboss:jboss-transaction-spi:jar:8.0.0.Final:compile +[INFO] | | \- io.agroal:agroal-pool:jar:2.8:compile +[INFO] | +- io.quarkus:quarkus-datasource-common:jar:3.31.1:compile +[INFO] | \- io.quarkus:quarkus-narayana-jta:jar:3.31.1:compile +[INFO] | +- io.quarkus:quarkus-transaction-annotations:jar:3.31.1:compile +[INFO] | +- io.smallrye:smallrye-context-propagation-jta:jar:2.3.0:compile +[INFO] | +- io.smallrye.reactive:smallrye-reactive-converter-api:jar:3.0.3:compile +[INFO] | +- io.smallrye.reactive:smallrye-reactive-converter-mutiny:jar:3.0.3:compile +[INFO] | +- io.smallrye.reactive:mutiny-zero-flow-adapters:jar:1.1.1:compile +[INFO] | +- org.jboss.narayana.jta:narayana-jta:jar:7.3.3.Final:compile +[INFO] | | +- jakarta.resource:jakarta.resource-api:jar:2.1.0:compile +[INFO] | | +- org.jboss.invocation:jboss-invocation:jar:2.0.0.Final:compile +[INFO] | | \- org.eclipse.microprofile.reactive-streams-operators:microprofile-reactive-streams-operators-api:jar:3.0.1:compile +[INFO] | \- org.jboss.narayana.jts:narayana-jts-integration:jar:7.3.3.Final:compile +[INFO] +- io.quarkus:quarkus-hibernate-orm-panache:jar:3.31.1:compile +[INFO] | +- io.quarkus:quarkus-hibernate-orm:jar:3.31.1:compile +[INFO] | | +- org.hibernate.orm:hibernate-core:jar:7.2.1.Final:compile +[INFO] | | | +- org.hibernate.models:hibernate-models:jar:1.0.1:runtime +[INFO] | | | \- org.antlr:antlr4-runtime:jar:4.13.2:compile +[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.17.8:compile +[INFO] | | +- org.hibernate.orm:hibernate-graalvm:jar:7.2.1.Final:compile +[INFO] | | +- jakarta.persistence:jakarta.persistence-api:jar:3.2.0:compile +[INFO] | | +- org.hibernate.local-cache:quarkus-local-cache:jar:0.5.0:compile +[INFO] | | \- io.quarkus:quarkus-caffeine:jar:3.31.1:compile +[INFO] | | \- com.github.ben-manes.caffeine:caffeine:jar:3.2.3:compile +[INFO] | | \- com.google.errorprone:error_prone_annotations:jar:2.46.0:compile +[INFO] | +- io.quarkus:quarkus-hibernate-orm-panache-common:jar:3.31.1:compile +[INFO] | | \- io.quarkus:quarkus-panache-hibernate-common:jar:3.31.1:compile +[INFO] | \- io.quarkus:quarkus-panache-common:jar:3.31.1:compile +[INFO] +- io.quarkus:quarkus-rest-client-config:jar:3.31.1:compile +[INFO] | +- io.quarkus:quarkus-proxy-registry:jar:3.31.1:compile +[INFO] | +- org.eclipse.microprofile.rest.client:microprofile-rest-client-api:jar:4.0:compile +[INFO] | \- io.smallrye:jandex:jar:3.5.3:compile +[INFO] +- io.quarkus:quarkus-rest-client-jackson:jar:3.31.1:compile +[INFO] | +- io.quarkus.resteasy.reactive:resteasy-reactive-jackson:jar:3.31.1:compile +[INFO] | | \- com.fasterxml.jackson.core:jackson-databind:jar:2.20.1:compile +[INFO] | | \- com.fasterxml.jackson.core:jackson-annotations:jar:2.20:compile +[INFO] | \- io.quarkus:quarkus-rest-client:jar:3.31.1:compile +[INFO] | \- io.quarkus:quarkus-rest-client-jaxrs:jar:3.31.1:compile +[INFO] | \- io.quarkus.resteasy.reactive:resteasy-reactive-client:jar:3.31.1:compile +[INFO] | +- io.smallrye.stork:stork-api:jar:2.7.3:compile +[INFO] | \- io.vertx:vertx-web-client:jar:4.5.24:compile +[INFO] +- org.projectlombok:lombok:jar:1.18.42:provided +[INFO] +- org.junit.jupiter:junit-jupiter:jar:5.10.3:test +[INFO] | +- org.junit.jupiter:junit-jupiter-api:jar:6.0.2:test +[INFO] | | +- org.opentest4j:opentest4j:jar:1.3.0:test +[INFO] | | +- org.junit.platform:junit-platform-commons:jar:6.0.2:test +[INFO] | | +- org.apiguardian:apiguardian-api:jar:1.1.2:test +[INFO] | | \- org.jspecify:jspecify:jar:1.0.0:compile +[INFO] | +- org.junit.jupiter:junit-jupiter-params:jar:6.0.2:test +[INFO] | \- org.junit.jupiter:junit-jupiter-engine:jar:6.0.2:test +[INFO] | \- org.junit.platform:junit-platform-engine:jar:6.0.2:test +[INFO] \- org.mockito:mockito-junit-jupiter:jar:5.12.0:test +[INFO] \- org.mockito:mockito-core:jar:5.21.0:test +[INFO] +- net.bytebuddy:byte-buddy-agent:jar:1.17.8:test +[INFO] \- org.objenesis:objenesis:jar:3.3:test +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 1.062 s +[INFO] Finished at: 2026-02-21T19:02:53+01:00 +[INFO] ------------------------------------------------------------------------