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