add Hibernate to kontor-javalin

This commit is contained in:
2025-12-30 14:43:48 +01:00
parent d4dbfa58e9
commit 15a0c8701c
27 changed files with 532 additions and 95 deletions
+1
View File
@@ -16,6 +16,7 @@ dependencies {
implementation libs.postgresql
implementation libs.hibernate
implementation libs.hibernate.validator
implementation libs.hypersistence
implementation libs.validation.api
annotationProcessor libs.hibernate.jpamodelgen
testImplementation(platform(libs.junit.bom))
+1 -1
View File
@@ -1,3 +1,3 @@
description='Kontor with Spring Boot'
description='Kontor with Javalin'
version=0.2.0-SNAPSHOT
group=de.thpeetz
+2
View File
@@ -13,6 +13,7 @@ lombok = "1.18.34"
postgresql = "42.7.3"
hibernate = "7.0.5.Final"
validation = "2.0.1.Final"
hypersistence = "3.14.1"
[libraries]
junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
@@ -29,3 +30,4 @@ hibernate = { module = "org.hibernate.orm:hibernate-core", version.ref = "hibern
hibernate-jpamodelgen = { module = "org.hibernate.orm:hibernate-jpamodelgen", version.ref = "hibernate" }
hibernate-validator = { module = "org.hibernate:hibernate-validator", version.ref = "hibernate" }
validation-api = { module = "javax.validation:validation-api", version.ref = "validation" }
hypersistence = { module = "io.hypersistence:hypersistence-utils-hibernate-70", version.ref = "hypersistence" }
@@ -0,0 +1,33 @@
package de.thpeetz.kontor;
import io.javalin.Javalin;
import static io.javalin.apibuilder.ApiBuilder.get;
import static io.javalin.apibuilder.ApiBuilder.path;
import static io.javalin.apibuilder.ApiBuilder.post;
import java.util.HashMap;
import de.thpeetz.kontor.web.ComicHandler;
import de.thpeetz.kontor.web.MediaFileHandler;
public class JavalinApp {
public static Javalin create() {
return Javalin.create((var config) -> config.router.apiBuilder(() -> {
path("/", () -> get(ctx -> ctx.json("Ok")));
path("health", () -> get(ctx -> {
HashMap<String, String> status = new HashMap<>();
status.put("status", "ok");
ctx.json(status);
}));
path("/api/v1/comics", () -> {
get(ComicHandler.listAll);
});
path("/api/v1/media/files", () -> {
get(MediaFileHandler.listAll);
post(MediaFileHandler.save);
});
}));
}
}
@@ -0,0 +1,15 @@
package de.thpeetz.kontor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static Logger logger = LoggerFactory.getLogger(Main.class);
private static short port = 8400;
public static void main(String[] args) {
JavalinApp.create().start(port);
logger.info("API's alive for real :-)");
}
}
@@ -1,37 +0,0 @@
package de.thpeetz.kontor.api;
import de.thpeetz.kontor.services.inmemory.InMemoryPersonReader;
import io.javalin.Javalin;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
private static Logger logger = LoggerFactory.getLogger(Main.class);
private static short port = 8400;
public static void main(String[] args) {
var personReader = new InMemoryPersonReader();
var objMapper = new ObjectMapper();
var result = objMapper.valueToTree(personReader.getAll());
logger.info("API: found {} people.", personReader.getAll().size());
var app = Javalin.create().start(port);
app.get("/ping", ctx -> ctx.result("pong"));
app.get("/health", ctx -> {
HashMap<String, String> status = new HashMap<>();
status.put("status", "ok");
ctx.json(status);
});
app.get("/persons", ctx -> {
logger.info("persons called");
ctx.json(result);
});
logger.info("API's alive for real :-)))");
}
}
@@ -0,0 +1,17 @@
package de.thpeetz.kontor.infrastructure;
import java.util.function.Consumer;
import java.util.function.Function;
import org.hibernate.StatelessSession;
public class AppHibernate {
public static void inTransaction(Consumer<StatelessSession> consumer) {
AppHibernateSessionFactory.getSessionFactory().inStatelessTransaction(consumer);
}
public static <R> R fromTransaction(Function<StatelessSession, R> function) {
return AppHibernateSessionFactory.getSessionFactory().fromStatelessTransaction(function);
}
}
@@ -0,0 +1,54 @@
package de.thpeetz.kontor.infrastructure;
import java.util.Properties;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.tool.schema.Action;
import de.thpeetz.kontor.models.comics.Artist;
import de.thpeetz.kontor.models.comics.Comic;
import de.thpeetz.kontor.models.comics.ComicWork;
import de.thpeetz.kontor.models.comics.Issue;
import de.thpeetz.kontor.models.comics.IssueWork;
import de.thpeetz.kontor.models.comics.Publisher;
import de.thpeetz.kontor.models.comics.StoryArc;
import de.thpeetz.kontor.models.comics.TradePaperback;
import de.thpeetz.kontor.models.comics.Volume;
import de.thpeetz.kontor.models.comics.Worktype;
import de.thpeetz.kontor.models.media.MediaActor;
import de.thpeetz.kontor.models.media.MediaActorFile;
import de.thpeetz.kontor.models.media.MediaArticle;
import de.thpeetz.kontor.models.media.MediaFile;
import de.thpeetz.kontor.models.media.MediaVideo;
class AppHibernateConfig {
static Configuration configuration() {
var configuration = new Configuration();
var settings = new Properties();
settings.put(AvailableSettings.JAKARTA_JDBC_DRIVER, "org.postgresql.Driver");
settings.put(AvailableSettings.JAKARTA_JDBC_URL, "jdbc:postgresql://postgres:5432/kontor");
settings.put(AvailableSettings.JAKARTA_JDBC_USER, "kontor");
settings.put(AvailableSettings.JAKARTA_JDBC_PASSWORD, "kontor");
settings.put(AvailableSettings.HIGHLIGHT_SQL, true);
settings.put(AvailableSettings.HBM2DDL_AUTO, Action.ACTION_CREATE);
configuration.setProperties(settings);
configuration.addAnnotatedClass(MediaFile.class);
configuration.addAnnotatedClass(MediaActorFile.class);
configuration.addAnnotatedClass(MediaActor.class);
configuration.addAnnotatedClass(MediaArticle.class);
configuration.addAnnotatedClass(MediaVideo.class);
configuration.addAnnotatedClass(Comic.class);
configuration.addAnnotatedClass(Publisher.class);
configuration.addAnnotatedClass(Artist.class);
configuration.addAnnotatedClass(ComicWork.class);
configuration.addAnnotatedClass(Issue.class);
configuration.addAnnotatedClass(IssueWork.class);
configuration.addAnnotatedClass(StoryArc.class);
configuration.addAnnotatedClass(TradePaperback.class);
configuration.addAnnotatedClass(Volume.class);
configuration.addAnnotatedClass(Worktype.class);
return configuration;
}
}
@@ -0,0 +1,30 @@
package de.thpeetz.kontor.infrastructure;
import java.util.Objects;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class AppHibernateSessionFactory {
private static final Logger logger = LoggerFactory.getLogger(AppHibernateSessionFactory.class);
private static SessionFactory sessionFactory;
static SessionFactory getSessionFactory() {
if (Objects.isNull(sessionFactory)) {
try {
var configuration = AppHibernateConfig.configuration();
var serviceRegistry = new StandardServiceRegistryBuilder()
.applySettings(configuration.getProperties())
.build();
sessionFactory = configuration.buildSessionFactory(serviceRegistry);
} catch (Throwable ex) {
logger.error("Failed to create session factory", ex);
}
}
return sessionFactory;
}
}
@@ -1,27 +0,0 @@
package de.thpeetz.kontor.models;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@@ -4,8 +4,6 @@ import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import org.checkerframework.checker.units.qual.Volume;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.CascadeType;
@@ -51,8 +49,10 @@ public class Comic {
@JsonIgnoreProperties({ "comics" })
private Publisher publisher;
@Column
private Boolean currentOrder = false;
@Column
private Boolean completed = false;
@Column(nullable = true)
@@ -0,0 +1,12 @@
package de.thpeetz.kontor.models.comics;
import java.util.List;
import org.hibernate.StatelessSession;
import org.hibernate.annotations.processing.Find;
public interface ComicQueries {
@Find
List<Comic> getAllComics(StatelessSession session);
}
@@ -4,10 +4,8 @@ import java.time.YearMonth;
import java.util.Date;
import java.util.List;
import org.checkerframework.checker.units.qual.Volume;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.hypersistence.utils.hibernate.type.basic.YearMonthDateType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -98,7 +96,7 @@ public class Issue {
stringBuilder.append(this.getComic().getTitle());
stringBuilder.append(" #");
stringBuilder.append(this.getIssueNumber());
if (this.title !=null && !this.title.isEmpty()) {
if (this.title != null && !this.title.isEmpty()) {
stringBuilder.append(": ");
stringBuilder.append(this.title);
}
@@ -11,9 +11,13 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table
@Getter
@Setter
public class IssueWork {
@Id
@@ -0,0 +1,51 @@
package de.thpeetz.kontor.models.media;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotEmpty;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table
public class MediaActor {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@NotEmpty
@Column(unique = true)
private String name;
@Column(nullable = true)
private String url;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "media_actor", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<MediaActorFile> mediaActorFiles = new LinkedList<>();
}
@@ -0,0 +1,56 @@
package de.thpeetz.kontor.models.media;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(indexes = {
@Index(columnList = "media_file_id, media_actor_id") }, uniqueConstraints = @UniqueConstraint(columnNames = {
"media_file_id", "media_actor_id" }))
public class MediaActorFile {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@ManyToOne
@JoinColumn(name = "media_file_id")
@NotNull
private MediaFile media_file;
@ManyToOne
@JoinColumn(name = "media_actor_id")
@NotNull
private MediaActor media_actor;
public String getTitle() {
return media_file.getTitle();
}
}
@@ -0,0 +1,45 @@
package de.thpeetz.kontor.models.media;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = { "url" }))
public class MediaArticle {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date", nullable = true)
private Date lastModifiedDate;
@NotEmpty
private String url;
@Column
private boolean review;
@Column(nullable = true)
private String title;
}
@@ -0,0 +1,71 @@
package de.thpeetz.kontor.models.media;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table
public class MediaFile {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date")
private Date lastModifiedDate;
@Column(nullable = true)
private String url;
@Column
private boolean review;
@Column
private boolean shouldDownload;
@Column(nullable = true)
private String title;
@Column(nullable = true)
private String cloudLink;
@Column(nullable = true)
private String fileName;
@Column(nullable = true)
private String path;
@Column(nullable = true)
@OneToMany(fetch = FetchType.EAGER, mappedBy = "media_file", cascade = CascadeType.REFRESH, orphanRemoval = true)
List<MediaActorFile> mediaActorFiles = new LinkedList<>();
public static MediaFile newMediaFile(String url) {
var mediaFile = new MediaFile();
mediaFile.setUrl(url);
return mediaFile;
}
}
@@ -0,0 +1,11 @@
package de.thpeetz.kontor.models.media;
import java.util.List;
import org.hibernate.annotations.processing.Find;
public interface MediaFileQueries {
@Find
List<MediaFile> getAllMediaFiles();
}
@@ -0,0 +1,57 @@
package de.thpeetz.kontor.models.media;
import java.util.Date;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) })
public class MediaVideo {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Version
private int version;
@Column(name = "created_date")
private Date createdDate;
@Column(name = "last_modified_date", nullable = true)
private Date lastModifiedDate;
@Column
private String url;
@Column
private boolean review;
@Column
private boolean shouldDownload;
@Column(nullable = true)
private String title;
@Column(nullable = true)
private String cloudLink;
@Column(nullable = true)
private String fileName;
@Column(nullable = true)
private String path;
}
@@ -1,8 +0,0 @@
package de.thpeetz.kontor.services.api;
import de.thpeetz.kontor.models.Person;
import java.util.List;
public interface PersonReader {
List<Person> getAll();
}
@@ -1,16 +0,0 @@
package de.thpeetz.kontor.services.inmemory;
import de.thpeetz.kontor.models.Person;
import java.util.List;
import de.thpeetz.kontor.services.api.PersonReader;
public class InMemoryPersonReader implements PersonReader {
@Override
public List<Person> getAll() {
return List.of(
new Person("Vincent Vega", 73),
new Person("Jules Winnfield", 12));
}
}
@@ -0,0 +1,17 @@
package de.thpeetz.kontor.web;
import java.util.List;
import de.thpeetz.kontor.infrastructure.AppHibernate;
import de.thpeetz.kontor.models.comics.Comic;
import de.thpeetz.kontor.models.comics.ComicQueries_;
import de.thpeetz.kontor.web.model.ResultComic;
import io.javalin.http.Handler;
public class ComicHandler {
public static Handler listAll = (context) -> {
List<Comic> result = AppHibernate.fromTransaction(ComicQueries_::getAllComics);
context.json(new ResultComic(result));
};
}
@@ -0,0 +1,30 @@
package de.thpeetz.kontor.web;
import java.util.List;
import de.thpeetz.kontor.infrastructure.AppHibernate;
import io.javalin.http.Handler;
import io.javalin.http.HttpStatus;
import de.thpeetz.kontor.models.media.MediaFile;
import de.thpeetz.kontor.models.media.MediaFileQueries;
import de.thpeetz.kontor.models.media.MediaFileQueries_;
import de.thpeetz.kontor.web.model.NewMediaFile;
import de.thpeetz.kontor.web.model.ResultMediaFile;
public class MediaFileHandler {
public static Handler listAll = (context) -> {
List<MediaFile> result = AppHibernate.fromTransaction(MediaFileQueries_::getAllMediaFiles);
context.json(new ResultMediaFile(result));
};
public static Handler save = (context) -> {
var newMediaFile = context.bodyAsClass(NewMediaFile.class);
var result = AppHibernate.fromTransaction(session -> {
var insertedId = session.insert(MediaFile.newMediaFile(newMediaFile.url()));
return session.get(MediaFile.class, insertedId);
});
context.json(result).status(HttpStatus.CREATED);
};
}
@@ -0,0 +1,4 @@
package de.thpeetz.kontor.web.model;
public record NewMediaFile(String url) {
}
@@ -0,0 +1,8 @@
package de.thpeetz.kontor.web.model;
import java.util.List;
import de.thpeetz.kontor.models.comics.Comic;
public record ResultComic(List<Comic> comics) {
}
@@ -0,0 +1,9 @@
package de.thpeetz.kontor.web.model;
import java.util.List;
import de.thpeetz.kontor.models.media.MediaFile;
public record ResultMediaFile(List<MediaFile> mediaFiles) {
}