summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gradle/libs.versions.toml2
-rw-r--r--opendc-web/opendc-web-api/build.gradle.kts1
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt9
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt9
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt9
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt74
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt26
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt83
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt107
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt38
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt48
-rw-r--r--opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt149
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/application-dev.properties2
-rw-r--r--opendc-web/opendc-web-api/src/main/resources/application-test.properties2
14 files changed, 539 insertions, 20 deletions
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8967b8eb..4411bd87 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -25,7 +25,6 @@ opentelemetry-semconv = "1.11.0-alpha"
parquet = "1.12.2"
progressbar = "0.9.2"
quarkus = "2.7.5.Final"
-quarkus-hibernate-types = "0.2.0"
quarkus-junit5-mockk = "0.3.0"
sentry = "5.5.2"
shadow = "7.1.2"
@@ -83,7 +82,6 @@ quarkus-security = { module = "io.quarkus:quarkus-security" }
quarkus-oidc = { module = "io.quarkus:quarkus-oidc" }
quarkus-hibernate-orm = { module = "io.quarkus:quarkus-hibernate-orm" }
quarkus-hibernate-validator = { module = "io.quarkus:quarkus-hibernate-validator" }
-quarkus-hibernate-types = { module = "io.quarkiverse.hibernatetypes:quarkus-hibernate-types", version.ref = "quarkus-hibernate-types" }
quarkus-jdbc-h2 = { module = "io.quarkus:quarkus-jdbc-h2" }
quarkus-jdbc-postgresql = { module = "io.quarkus:quarkus-jdbc-postgresql" }
quarkus-flyway = { module = "io.quarkus:quarkus-flyway" }
diff --git a/opendc-web/opendc-web-api/build.gradle.kts b/opendc-web/opendc-web-api/build.gradle.kts
index 9889b832..488ce8af 100644
--- a/opendc-web/opendc-web-api/build.gradle.kts
+++ b/opendc-web/opendc-web-api/build.gradle.kts
@@ -48,7 +48,6 @@ dependencies {
implementation(libs.quarkus.hibernate.orm)
implementation(libs.quarkus.hibernate.validator)
- implementation(libs.quarkus.hibernate.types)
implementation(libs.quarkus.jdbc.postgresql)
quarkusDev(libs.quarkus.jdbc.h2)
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt
index 23838e34..b09b46a1 100644
--- a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Job.kt
@@ -22,10 +22,9 @@
package org.opendc.web.api.model
-import io.quarkiverse.hibernate.types.json.JsonBinaryType
-import io.quarkiverse.hibernate.types.json.JsonTypes
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
import org.opendc.web.proto.JobState
import java.time.Instant
import javax.persistence.*
@@ -33,7 +32,7 @@ import javax.persistence.*
/**
* A simulation job to be run by the simulator.
*/
-@TypeDef(name = JsonTypes.JSON_BIN, typeClass = JsonBinaryType::class)
+@TypeDef(name = "json", typeClass = JsonType::class)
@Entity
@Table(name = "jobs")
@NamedQueries(
@@ -85,8 +84,8 @@ class Job(
/**
* Experiment results in JSON
*/
- @Type(type = JsonTypes.JSON_BIN)
- @Column(columnDefinition = JsonTypes.JSON_BIN)
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb")
var results: Map<String, Any>? = null
/**
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt
index 9a383c7c..5c9cb259 100644
--- a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Scenario.kt
@@ -22,17 +22,16 @@
package org.opendc.web.api.model
-import io.quarkiverse.hibernate.types.json.JsonBinaryType
-import io.quarkiverse.hibernate.types.json.JsonTypes
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
import org.opendc.web.proto.OperationalPhenomena
import javax.persistence.*
/**
* A single scenario to be explored by the simulator.
*/
-@TypeDef(name = JsonTypes.JSON_BIN, typeClass = JsonBinaryType::class)
+@TypeDef(name = "json", typeClass = JsonType::class)
@Entity
@Table(
name = "scenarios",
@@ -88,8 +87,8 @@ class Scenario(
@ManyToOne(optional = false)
val topology: Topology,
- @Type(type = JsonTypes.JSON_BIN)
- @Column(columnDefinition = JsonTypes.JSON_BIN, nullable = false, updatable = false)
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb", nullable = false, updatable = false)
val phenomena: OperationalPhenomena,
@Column(name = "scheduler_name", nullable = false, updatable = false)
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt
index 32bf799a..9b64e382 100644
--- a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/model/Topology.kt
@@ -22,10 +22,9 @@
package org.opendc.web.api.model
-import io.quarkiverse.hibernate.types.json.JsonBinaryType
-import io.quarkiverse.hibernate.types.json.JsonTypes
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
+import org.opendc.web.api.util.hibernate.json.JsonType
import org.opendc.web.proto.Room
import java.time.Instant
import javax.persistence.*
@@ -33,7 +32,7 @@ import javax.persistence.*
/**
* A datacenter design in OpenDC.
*/
-@TypeDef(name = JsonTypes.JSON_BIN, typeClass = JsonBinaryType::class)
+@TypeDef(name = "json", typeClass = JsonType::class)
@Entity
@Table(
name = "topologies",
@@ -76,8 +75,8 @@ class Topology(
/**
* Datacenter design in JSON
*/
- @Type(type = JsonTypes.JSON_BIN)
- @Column(columnDefinition = JsonTypes.JSON_BIN, nullable = false)
+ @Type(type = "json")
+ @Column(columnDefinition = "jsonb", nullable = false)
var rooms: List<Room> = emptyList()
) {
/**
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
new file mode 100644
index 00000000..134739c9
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.type.descriptor.ValueExtractor
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicExtractor
+import org.hibernate.type.descriptor.sql.SqlTypeDescriptor
+import java.sql.CallableStatement
+import java.sql.ResultSet
+import java.sql.Types
+
+/**
+ * Abstract implementation of a [SqlTypeDescriptor] for Hibernate JSON type.
+ */
+internal abstract class AbstractJsonSqlTypeDescriptor : SqlTypeDescriptor {
+
+ override fun getSqlType(): Int {
+ return Types.OTHER
+ }
+
+ override fun canBeRemapped(): Boolean {
+ return true
+ }
+
+ override fun <X> getExtractor(typeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> {
+ return object : BasicExtractor<X>(typeDescriptor, this) {
+ override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X {
+ return typeDescriptor.wrap(extractJson(rs, name), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X {
+ return typeDescriptor.wrap(extractJson(statement, index), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X {
+ return typeDescriptor.wrap(extractJson(statement, name), options)
+ }
+ }
+ }
+
+ open fun extractJson(rs: ResultSet, name: String): Any? {
+ return rs.getObject(name)
+ }
+
+ open fun extractJson(statement: CallableStatement, index: Int): Any? {
+ return statement.getObject(index)
+ }
+
+ open fun extractJson(statement: CallableStatement, name: String): Any? {
+ return statement.getObject(name)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
new file mode 100644
index 00000000..32f69928
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt
@@ -0,0 +1,26 @@
+package org.opendc.web.api.util.hibernate.json
+
+import com.fasterxml.jackson.databind.JsonNode
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import java.sql.CallableStatement
+import java.sql.PreparedStatement
+
+/**
+ * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as binary (JSONB).
+ */
+internal object JsonBinarySqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
+ override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(typeDescriptor, this) {
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ st.setObject(index, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType)
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ st.setObject(name, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType)
+ }
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
new file mode 100644
index 00000000..eaecc5b0
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import java.io.UnsupportedEncodingException
+import java.sql.*
+
+/**
+ * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as UTF-8 encoded bytes.
+ */
+internal object JsonBytesSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
+ private val CHARSET = Charsets.UTF_8
+
+ override fun getSqlType(): Int {
+ return Types.BINARY
+ }
+
+ override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(javaTypeDescriptor, this) {
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ st.setBytes(index, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options)))
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ st.setBytes(name, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options)))
+ }
+ }
+ }
+
+ override fun extractJson(rs: ResultSet, name: String): Any? {
+ return fromJsonBytes(rs.getBytes(name))
+ }
+
+ override fun extractJson(statement: CallableStatement, index: Int): Any? {
+ return fromJsonBytes(statement.getBytes(index))
+ }
+
+ override fun extractJson(statement: CallableStatement, name: String): Any? {
+ return fromJsonBytes(statement.getBytes(name))
+ }
+
+ private fun toJsonBytes(jsonValue: String): ByteArray? {
+ return try {
+ jsonValue.toByteArray(CHARSET)
+ } catch (e: UnsupportedEncodingException) {
+ throw IllegalStateException(e)
+ }
+ }
+
+ private fun fromJsonBytes(jsonBytes: ByteArray?): String? {
+ return if (jsonBytes == null) {
+ null
+ } else try {
+ String(jsonBytes, CHARSET)
+ } catch (e: UnsupportedEncodingException) {
+ throw IllegalStateException(e)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt
new file mode 100644
index 00000000..e005f368
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonSqlTypeDescriptor.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.dialect.H2Dialect
+import org.hibernate.dialect.PostgreSQL81Dialect
+import org.hibernate.internal.SessionImpl
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.ValueExtractor
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import org.hibernate.type.descriptor.sql.BasicExtractor
+import org.hibernate.type.descriptor.sql.SqlTypeDescriptor
+import java.sql.*
+
+/**
+ * A [SqlTypeDescriptor] that automatically selects the correct implementation for the database dialect.
+ */
+internal object JsonSqlTypeDescriptor : SqlTypeDescriptor {
+
+ override fun getSqlType(): Int = Types.OTHER
+
+ override fun canBeRemapped(): Boolean = true
+
+ override fun <X> getExtractor(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> {
+ return object : BasicExtractor<X>(javaTypeDescriptor, this) {
+ private var delegate: AbstractJsonSqlTypeDescriptor? = null
+
+ override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X {
+ return javaTypeDescriptor.wrap(delegate(options).extractJson(rs, name), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X {
+ return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, index), options)
+ }
+
+ override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X {
+ return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, name), options)
+ }
+
+ private fun delegate(options: WrapperOptions): AbstractJsonSqlTypeDescriptor {
+ var delegate = delegate
+ if (delegate == null) {
+ delegate = resolveSqlTypeDescriptor(options)
+ this.delegate = delegate
+ }
+ return delegate
+ }
+ }
+ }
+
+ override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(javaTypeDescriptor, this) {
+ private var delegate: ValueBinder<X>? = null
+
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ delegate(options).bind(st, value, index, options)
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ delegate(options).bind(st, value, name, options)
+ }
+
+ private fun delegate(options: WrapperOptions): ValueBinder<X> {
+ var delegate = delegate
+ if (delegate == null) {
+ delegate = checkNotNull(resolveSqlTypeDescriptor(options).getBinder(javaTypeDescriptor))
+ this.delegate = delegate
+ }
+ return delegate
+ }
+ }
+ }
+
+ /**
+ * Helper method to resolve the appropriate [SqlTypeDescriptor] based on the [WrapperOptions].
+ */
+ private fun resolveSqlTypeDescriptor(options: WrapperOptions): AbstractJsonSqlTypeDescriptor {
+ val session = options as? SessionImpl
+ return when (session?.jdbcServices?.dialect) {
+ is PostgreSQL81Dialect -> JsonBinarySqlTypeDescriptor
+ is H2Dialect -> JsonBytesSqlTypeDescriptor
+ else -> JsonStringSqlTypeDescriptor
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
new file mode 100644
index 00000000..cf400c95
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonStringSqlTypeDescriptor.kt
@@ -0,0 +1,38 @@
+package org.opendc.web.api.util.hibernate.json
+
+import org.hibernate.type.descriptor.ValueBinder
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.JavaTypeDescriptor
+import org.hibernate.type.descriptor.sql.BasicBinder
+import java.sql.*
+
+/**
+ * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as string (VARCHAR).
+ */
+internal object JsonStringSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() {
+ override fun getSqlType(): Int = Types.VARCHAR
+
+ override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> {
+ return object : BasicBinder<X>(typeDescriptor, this) {
+ override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) {
+ st.setString(index, typeDescriptor.unwrap(value, String::class.java, options))
+ }
+
+ override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) {
+ st.setString(name, typeDescriptor.unwrap(value, String::class.java, options))
+ }
+ }
+ }
+
+ override fun extractJson(rs: ResultSet, name: String): Any? {
+ return rs.getString(name)
+ }
+
+ override fun extractJson(statement: CallableStatement, index: Int): Any? {
+ return statement.getString(index)
+ }
+
+ override fun extractJson(statement: CallableStatement, name: String): Any? {
+ return statement.getString(name)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt
new file mode 100644
index 00000000..2206e82f
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonType.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.hibernate.type.AbstractSingleColumnStandardBasicType
+import org.hibernate.type.BasicType
+import org.hibernate.usertype.DynamicParameterizedType
+import java.util.*
+import javax.enterprise.inject.spi.CDI
+
+/**
+ * A [BasicType] that contains JSON.
+ */
+class JsonType(objectMapper: ObjectMapper) : AbstractSingleColumnStandardBasicType<Any>(JsonSqlTypeDescriptor, JsonTypeDescriptor(objectMapper)), DynamicParameterizedType {
+ /**
+ * No-arg constructor for Hibernate to instantiate.
+ */
+ constructor() : this(CDI.current().select(ObjectMapper::class.java).get())
+
+ override fun getName(): String = "json"
+
+ override fun registerUnderJavaType(): Boolean = true
+
+ override fun setParameterValues(parameters: Properties) {
+ (javaTypeDescriptor as JsonTypeDescriptor).setParameterValues(parameters)
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt
new file mode 100644
index 00000000..3386582e
--- /dev/null
+++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/hibernate/json/JsonTypeDescriptor.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2022 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.web.api.util.hibernate.json
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.hibernate.HibernateException
+import org.hibernate.annotations.common.reflection.XProperty
+import org.hibernate.annotations.common.reflection.java.JavaXMember
+import org.hibernate.engine.jdbc.BinaryStream
+import org.hibernate.engine.jdbc.internal.BinaryStreamImpl
+import org.hibernate.type.descriptor.WrapperOptions
+import org.hibernate.type.descriptor.java.AbstractTypeDescriptor
+import org.hibernate.type.descriptor.java.BlobTypeDescriptor
+import org.hibernate.type.descriptor.java.DataHelper
+import org.hibernate.type.descriptor.java.MutableMutabilityPlan
+import org.hibernate.usertype.DynamicParameterizedType
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.lang.reflect.Type
+import java.sql.Blob
+import java.sql.SQLException
+import java.util.*
+
+/**
+ * An [AbstractTypeDescriptor] implementation for Hibernate JSON type.
+ */
+internal class JsonTypeDescriptor(private val objectMapper: ObjectMapper) : AbstractTypeDescriptor<Any>(Any::class.java, JsonMutabilityPlan(objectMapper)), DynamicParameterizedType {
+ private var type: Type? = null
+
+ override fun setParameterValues(parameters: Properties) {
+ val xProperty = parameters[DynamicParameterizedType.XPROPERTY] as XProperty
+ type = if (xProperty is JavaXMember) {
+ val x = xProperty as JavaXMember
+ x.javaType
+ } else {
+ (parameters[DynamicParameterizedType.PARAMETER_TYPE] as DynamicParameterizedType.ParameterType).returnedClass
+ }
+ }
+
+ override fun areEqual(one: Any?, another: Any?): Boolean {
+ return when {
+ one === another -> true
+ one == null || another == null -> false
+ one is String && another is String -> one == another
+ one is Collection<*> && another is Collection<*> -> Objects.equals(one, another)
+ else -> areJsonEqual(one, another)
+ }
+ }
+
+ override fun toString(value: Any?): String {
+ return objectMapper.writeValueAsString(value)
+ }
+
+ override fun fromString(string: String): Any? {
+ return objectMapper.readValue(string, objectMapper.typeFactory.constructType(type))
+ }
+
+ override fun <X> unwrap(value: Any?, type: Class<X>, options: WrapperOptions): X? {
+ if (value == null) {
+ return null
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return when {
+ String::class.java.isAssignableFrom(type) -> toString(value)
+ BinaryStream::class.java.isAssignableFrom(type) || ByteArray::class.java.isAssignableFrom(type) -> {
+ val stringValue = if (value is String) value else toString(value)
+ BinaryStreamImpl(DataHelper.extractBytes(ByteArrayInputStream(stringValue.toByteArray())))
+ }
+ Blob::class.java.isAssignableFrom(type) -> {
+ val stringValue = if (value is String) value else toString(value)
+ BlobTypeDescriptor.INSTANCE.fromString(stringValue)
+ }
+ Any::class.java.isAssignableFrom(type) -> toJsonType(value)
+ else -> throw unknownUnwrap(type)
+ } as X
+ }
+
+ override fun <X> wrap(value: X?, options: WrapperOptions): Any? {
+ if (value == null) {
+ return null
+ }
+
+ var blob: Blob? = null
+ if (Blob::class.java.isAssignableFrom(value.javaClass)) {
+ blob = options.lobCreator.wrap(value as Blob?)
+ } else if (ByteArray::class.java.isAssignableFrom(value.javaClass)) {
+ blob = options.lobCreator.createBlob(value as ByteArray?)
+ } else if (InputStream::class.java.isAssignableFrom(value.javaClass)) {
+ val inputStream = value as InputStream
+ blob = try {
+ options.lobCreator.createBlob(inputStream, inputStream.available().toLong())
+ } catch (e: IOException) {
+ throw unknownWrap(value.javaClass)
+ }
+ }
+
+ val stringValue: String = try {
+ if (blob != null) String(DataHelper.extractBytes(blob.binaryStream)) else value.toString()
+ } catch (e: SQLException) {
+ throw HibernateException("Unable to extract binary stream from Blob", e)
+ }
+
+ return fromString(stringValue)
+ }
+
+ private class JsonMutabilityPlan(private val objectMapper: ObjectMapper) : MutableMutabilityPlan<Any>() {
+ override fun deepCopyNotNull(value: Any): Any {
+ return objectMapper.treeToValue(objectMapper.valueToTree(value), value.javaClass)
+ }
+ }
+
+ private fun readObject(value: String): Any {
+ return objectMapper.readTree(value)
+ }
+
+ private fun areJsonEqual(one: Any, another: Any): Boolean {
+ return readObject(objectMapper.writeValueAsString(one)) == readObject(objectMapper.writeValueAsString(another))
+ }
+
+ private fun toJsonType(value: Any?): Any {
+ return try {
+ readObject(objectMapper.writeValueAsString(value))
+ } catch (e: Exception) {
+ throw IllegalArgumentException(e)
+ }
+ }
+}
diff --git a/opendc-web/opendc-web-api/src/main/resources/application-dev.properties b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties
index 6f941067..84da528f 100644
--- a/opendc-web/opendc-web-api/src/main/resources/application-dev.properties
+++ b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties
@@ -20,7 +20,7 @@
# Datasource (H2)
quarkus.datasource.db-kind = h2
-quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS text;
+quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS blob;
# Hibernate
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
diff --git a/opendc-web/opendc-web-api/src/main/resources/application-test.properties b/opendc-web/opendc-web-api/src/main/resources/application-test.properties
index ce3a9473..0710f200 100644
--- a/opendc-web/opendc-web-api/src/main/resources/application-test.properties
+++ b/opendc-web/opendc-web-api/src/main/resources/application-test.properties
@@ -20,7 +20,7 @@
# Datasource configuration
quarkus.datasource.db-kind = h2
-quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE "JSONB" AS text;
+quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE "JSONB" AS blob;
quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
quarkus.hibernate-orm.database.generation=drop-and-create