Update dependencies, add character and scenario management features

- Upgrade Quarkus and OpenAPI generator versions in pom.xml.
- Introduce CharacterService and ScenarioService for managing character and scenario templates.
- Implement CharacterEntity and ScenarioEntity JPA entities with corresponding repositories.
- Add RESTful APIs for listing and retrieving characters and scenarios.
- Create JSON converter for persisting lists of strings in the database.
- Update OpenAPI specification to include new endpoints for character and scenario management.
- Add Liquibase migration scripts for character and scenario tables.
- Configure application settings for Hibernate ORM and database generation.
This commit is contained in:
Konrad Neitzel 2026-02-21 19:50:17 +01:00
parent cf93b35dd6
commit 3ce1215487
40 changed files with 2428 additions and 20 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

20
pom.xml
View File

@ -15,12 +15,14 @@
<maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.version>3.15.3</quarkus.platform.version>
<quarkus.platform.version>3.31.2</quarkus.platform.version>
<quarkus.plugin.version>${quarkus.platform.version}</quarkus.plugin.version>
<jackson.version>2.20.1</jackson.version>
<jackson.annotations.version>2.20</jackson.annotations.version>
<lombok.version>1.18.42</lombok.version>
<junit.jupiter.version>5.10.3</junit.jupiter.version>
<mockito.version>5.12.0</mockito.version>
<openapi.generator.version>7.11.0</openapi.generator.version>
<openapi.generator.version>7.13.0</openapi.generator.version>
<frontend.plugin.version>1.15.1</frontend.plugin.version>
<node.version>v22.13.1</node.version>
<npm.version>10.9.2</npm.version>
@ -68,6 +70,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-liquibase</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-config</artifactId>
@ -135,6 +141,7 @@
</execution>
</executions>
</plugin>
<!-- On Java 25, skip to avoid VerifyError; run JAVA_HOME=/path/to/jdk21 mvn generate-sources first. -->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
@ -148,6 +155,7 @@
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi-roleplay-public-v1.yml</inputSpec>
<generatorName>jaxrs-spec</generatorName>
<templateDirectory>${project.basedir}/src/main/resources/openapi-templates</templateDirectory>
<output>${project.build.directory}/generated-sources/openapi</output>
<apiPackage>de.neitzel.roleplay.generated.api</apiPackage>
<modelPackage>de.neitzel.roleplay.fascade.model</modelPackage>
@ -172,6 +180,7 @@
<goal>generate</goal>
</goals>
<configuration>
<skip>${skip.openapi.generate}</skip>
<inputSpec>${project.basedir}/src/main/resources/openapi-roleplay-public-v1.yml</inputSpec>
<generatorName>typescript-fetch</generatorName>
<output>${project.basedir}/src/main/web/src/api/generated</output>
@ -207,8 +216,11 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
<configuration>
<useModulePath>false</useModulePath>
<argLine>-Dnet.bytebuddy.experimental=true</argLine>
<argLine>@{argLine}</argLine>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>

View File

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

View File

@ -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.
*

View File

@ -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<ScenarioSummary> 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<ScenarioSetup> 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<ScenarioCharacterEntity> 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;
}
}

View File

@ -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<String> personalityTraits;
@Column(name = "speaking_style", length = 1000)
private String speakingStyle;
@Convert(converter = JsonListStringConverter.class)
@Column(name = "goals", columnDefinition = "clob")
private List<String> 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<String> getPersonalityTraits() {
return personalityTraits;
}
/**
* Sets the list of personality trait strings.
*/
public void setPersonalityTraits(final List<String> 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<String> getGoals() {
return goals;
}
/**
* Sets the list of goal strings.
*/
public void setGoals(final List<String> goals) {
this.goals = goals;
}
}

View File

@ -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<CharacterEntity> {
/**
* Returns all characters ordered by name.
*/
@Override
public List<CharacterEntity> 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();
}
}

View File

@ -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<List<String>, String> {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final TypeReference<List<String>> LIST_TYPE = new TypeReference<>() {};
@Override
public String convertToDatabaseColumn(final List<String> 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<String> 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);
}
}
}

View File

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

View File

@ -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<ScenarioCharacterEntity> 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 scenariocharacter links (user character first, then AI by position).
*/
public List<ScenarioCharacterEntity> getScenarioCharacters() {
return scenarioCharacters;
}
/**
* Sets the list of scenariocharacter links.
*/
public void setScenarioCharacters(final List<ScenarioCharacterEntity> scenarioCharacters) {
this.scenarioCharacters = scenarioCharacters;
}
}

View File

@ -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<ScenarioEntity> {
/**
* 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<ScenarioEntity> listAll() {
return list("ORDER BY name");
}
}

View File

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

View File

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

View File

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

View File

@ -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">
<include file="db/migration/v001__scenarios_and_characters.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<changeSet id="001-1-create-character-table" author="roleplay">
<createTable tableName="rp_character">
<column name="id" type="uuid">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="role" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="backstory" type="clob"/>
<column name="personality_traits" type="clob"/>
<column name="speaking_style" type="varchar(1000)"/>
<column name="goals" type="clob"/>
</createTable>
<comment>Stores reusable character definitions. personality_traits and goals stored as JSON text.</comment>
</changeSet>
<changeSet id="001-2-create-scenario-table" author="roleplay">
<createTable tableName="scenario">
<column name="id" type="uuid">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="setting" type="clob"/>
<column name="initial_conflict" type="clob"/>
</createTable>
<comment>Stores scenario templates (setting and initial conflict).</comment>
</changeSet>
<changeSet id="001-3-create-scenario-character-table" author="roleplay">
<createTable tableName="scenario_character">
<column name="id" type="uuid">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="scenario_id" type="uuid">
<constraints nullable="false" foreignKeyName="fk_scenario_character_scenario"
references="scenario(id)"/>
</column>
<column name="character_id" type="uuid">
<constraints nullable="false" foreignKeyName="fk_scenario_character_character"
references="rp_character(id)"/>
</column>
<column name="is_user_character" type="boolean">
<constraints nullable="false"/>
</column>
<column name="position" type="int">
<constraints nullable="false"/>
</column>
</createTable>
<comment>Links scenarios to characters; position orders AI characters.</comment>
</changeSet>
<changeSet id="001-4-seed-example-scenario" author="roleplay">
<insert tableName="rp_character">
<column name="id" value="11111111-1111-1111-1111-111111111101"/>
<column name="name" value="The Detective"/>
<column name="role" value="rookie detective"/>
<column name="backstory" value="Recently joined the force, eager to prove themselves."/>
<column name="personality_traits" value="[&quot;curious&quot;, &quot;determined&quot;]"/>
<column name="goals" value="[&quot;Solve the case&quot;, &quot;Earn respect&quot;]"/>
</insert>
<insert tableName="rp_character">
<column name="id" value="11111111-1111-1111-1111-111111111102"/>
<column name="name" value="Captain Morgan"/>
<column name="role" value="ship captain"/>
<column name="backstory" value="Veteran of many voyages; hides a secret."/>
<column name="personality_traits" value="[&quot;gruff&quot;, &quot;wary&quot;]"/>
<column name="goals" value="[&quot;Protect the crew&quot;, &quot;Keep the past buried&quot;]"/>
</insert>
<insert tableName="scenario">
<column name="id" value="22222222-2222-2222-2222-222222222201"/>
<column name="name" value="Harbour mystery"/>
<column name="setting" value="A fog-covered harbour at dawn, 1923"/>
<column name="initial_conflict" value="Strange noises from the cargo hold"/>
</insert>
<insert tableName="scenario_character">
<column name="id" value="33333333-3333-3333-3333-333333333301"/>
<column name="scenario_id" value="22222222-2222-2222-2222-222222222201"/>
<column name="character_id" value="11111111-1111-1111-1111-111111111101"/>
<column name="is_user_character" valueBoolean="true"/>
<column name="position" valueNumeric="0"/>
</insert>
<insert tableName="scenario_character">
<column name="id" value="33333333-3333-3333-3333-333333333302"/>
<column name="scenario_id" value="22222222-2222-2222-2222-222222222201"/>
<column name="character_id" value="11111111-1111-1111-1111-111111111102"/>
<column name="is_user_character" valueBoolean="false"/>
<column name="position" valueNumeric="1"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

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

View File

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

View File

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

View File

@ -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 ");
}
}

View File

@ -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<String, @Valid {{{items.datatypeWithEnum}}}>{{/items.isEnum}}{{/items.isPrimitiveType}}{{/isMap}}{{^isMap}}{{{datatypeWithEnum}}}{{/isMap}}{{/isMap}}{{/isArray}}{{/isContainer}}{{^isContainer}}{{{datatypeWithEnum}}}{{/isContainer}}

View File

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

View File

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

View File

@ -0,0 +1,2 @@
{{#additionalEnumTypeAnnotations}}{{{.}}}
{{/additionalEnumTypeAnnotations}}

View File

@ -0,0 +1,2 @@
{{#additionalModelTypeAnnotations}}{{{.}}}
{{/additionalModelTypeAnnotations}}

View File

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

View File

@ -0,0 +1 @@
{{#isArray}}{{baseType}}<{{#items}}{{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}}{{>beanValidatedType}}{{/items}}>{{/isArray}}{{^isArray}}{{{datatypeWithEnum}}}{{/isArray}}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
@{{javaxPackage}}.annotation.Generated(value = "{{generatorClass}}"{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}}, comments = "Generator version: {{generatorVersion}}")

View File

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

View File

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

View File

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

View File

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

View File

@ -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<boolean>(false)
const [error, setError] = useState<string | null>(null)
/** Saved scenarios from API; selectedScenarioId is '' for custom or a UUID. */
const [scenarios, setScenarios] = useState<ScenarioSummary[]>([])
const [selectedScenarioId, setSelectedScenarioId] = useState<string>('')
/** Optional scenario (collapsible). */
const [scenarioExpanded, setScenarioExpanded] = useState<boolean>(false)
const [setting, setSetting] = useState<string>('')
@ -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<string>) => {
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
/>
<FormControl fullWidth disabled={starting} sx={{mb: 1}}>
<InputLabel id="scenario-label">Saved scenario</InputLabel>
<Select
labelId="scenario-label"
value={selectedScenarioId}
label="Saved scenario"
onChange={handleScenarioSelect}
>
<MenuItem value="">
<em>Custom (none)</em>
</MenuItem>
{scenarios.map((s) => (
<MenuItem key={s.id} value={s.id}>
{s.name}
</MenuItem>
))}
</Select>
</FormControl>
<Accordion
expanded={scenarioExpanded}
onChange={() => setScenarioExpanded((b) => !b)}

View File

@ -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.
*/

View File

@ -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<ScenarioSummary> 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<ScenarioSummary> result = scenarioService.listScenarios();
assertTrue(result.isEmpty());
}
@Test
void getScenarioAsSetupReturnsEmptyWhenNotFound() {
UUID id = UUID.randomUUID();
when(scenarioRepository.findByIdWithCharacters(id)).thenReturn(null);
Optional<ScenarioSetup> 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<ScenarioSetup> 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());
}
}

216
tree.txt Normal file
View File

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