diff options
Diffstat (limited to 'opendc-web/opendc-web-api')
13 files changed, 539 insertions, 18 deletions
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 |
