Provide proxy kv, add tests for JPA store
diff --git a/build.gradle b/build.gradle
index b539938..f840203 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,7 +22,10 @@
repositories { jcenter() // jcenter
}
- dependencies { classpath "org.ajoberstar.grgit:grgit-core:3.1.1" }
+ dependencies {
+ classpath "org.ajoberstar.grgit:grgit-core:3.1.1"
+ classpath 'com.radcortez.gradle:openjpa-gradle-plugin:3.1.0'
+ }
}
plugins {
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index fa8d1cd..11b2eed 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -35,6 +35,7 @@
entry 'antlr4-runtime'
}
dependency('org.apache.lucene:lucene-core:7.6.0')
+ dependency('org.apache.openjpa:openjpa:3.1.0')
dependency('org.assertj:assertj-core:3.11.1')
dependencySet(group: 'org.bouncycastle', version: '1.60') {
entry 'bcpkix-jdk15on'
diff --git a/kv/build.gradle b/kv/build.gradle
index d05b5c2..c6f8aa1 100644
--- a/kv/build.gradle
+++ b/kv/build.gradle
@@ -12,7 +12,40 @@
*/
description = 'Key value store implementations.'
+task openJPAEnhance {
+ description "Enhance JPA model classes using OpenJPA Enhancer"
+ dependsOn compileTestJava
+
+ doLast {
+ // define the entity classes
+ def entityFiles = sourceSets.test.output.asFileTree.filter { it.name == "Store.class" }
+
+ println "Enhancing with OpenJPA, the following files..."
+ entityFiles.getFiles().each { println it }
+
+ // define Ant task for Enhancer
+ ant.taskdef(
+ name : 'openjpac',
+ classpath : sourceSets.test.runtimeClasspath.asPath,
+ classname : 'org.apache.openjpa.ant.PCEnhancerTask'
+ )
+
+ // Run the OpenJPA Enhancer as an Ant task
+ // - see OpenJPA 'PCEnhancerTask' for supported arguments
+ // - this invocation of the enhancer adds support for a default-ctor
+ // - as well as ensuring JPA property use is valid.
+ ant.openjpac(
+ classpath: sourceSets.test.runtimeClasspath.asPath,
+ addDefaultConstructor: true,
+ enforcePropertyRestrictions: true) {
+ entityFiles.addToAntBuilder(ant, 'fileset', FileCollection.AntType.FileSet)
+ }
+ }
+}
+test { dependsOn openJPAEnhance }
+
dependencies {
+
compile project(':bytes')
compile project(':concurrent-coroutines')
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
@@ -41,6 +74,8 @@
testCompile 'org.junit.jupiter:junit-jupiter-params'
testCompile 'org.mapdb:mapdb'
testCompile 'org.rocksdb:rocksdbjni'
+ testRuntime 'org.apache.openjpa:openjpa'
+
testRuntime 'org.jetbrains.spek:spek-junit-platform-engine'
testRuntime 'org.junit.jupiter:junit-jupiter-engine'
diff --git a/kv/src/main/kotlin/org/apache/tuweni/kv/EntityManagerKeyValueStore.kt b/kv/src/main/kotlin/org/apache/tuweni/kv/EntityManagerKeyValueStore.kt
index 8b653ad..712aec9 100644
--- a/kv/src/main/kotlin/org/apache/tuweni/kv/EntityManagerKeyValueStore.kt
+++ b/kv/src/main/kotlin/org/apache/tuweni/kv/EntityManagerKeyValueStore.kt
@@ -16,7 +16,6 @@
*/
package org.apache.tuweni.kv
-import kotlin.jvm.Throws
import kotlinx.coroutines.Dispatchers
import java.io.IOException
@@ -27,7 +26,6 @@
import kotlin.coroutines.CoroutineContext
class EntityManagerKeyValueStore<K, V>
- @Throws(IOException::class)
constructor(
private val entityManagerProvider: () -> EntityManager,
private val entityClass: Class<V>,
@@ -44,12 +42,11 @@
* @throws IOException If an I/O error occurs.
*/
@JvmStatic
- @Throws(IOException::class)
fun <K, V> open(
entityManagerProvider: Supplier<EntityManager>,
entityClass: Class<V>,
idAccessor: Function<V, K>
- ) = EntityManagerKeyValueStore<K, V>(entityManagerProvider::get, entityClass, idAccessor::apply)
+ ) = EntityManagerKeyValueStore(entityManagerProvider::get, entityClass, idAccessor::apply)
}
override suspend fun get(key: K): V? {
@@ -63,6 +60,8 @@
}
}
+ suspend fun put(value: V) = put(idAccessor(value), value)
+
override suspend fun put(key: K, value: V) {
val em = entityManagerProvider()
em.transaction.begin()
@@ -76,8 +75,11 @@
override suspend fun keys(): Iterable<K> {
val em = entityManagerProvider()
- val query = em.createQuery(em.criteriaBuilder.createQuery(entityClass))
- val resultStream: Stream<V> = query.resultStream
+ val query = em.criteriaBuilder.createQuery(entityClass)
+ val root = query.from(entityClass)
+ val all = query.select(root)
+ val finalAll = em.createQuery(all)
+ val resultStream: Stream<V> = finalAll.resultStream
return Iterable { resultStream.map(idAccessor).iterator() }
}
diff --git a/kv/src/main/kotlin/org/apache/tuweni/kv/ProxyKeyValueStore.kt b/kv/src/main/kotlin/org/apache/tuweni/kv/ProxyKeyValueStore.kt
new file mode 100644
index 0000000..6d68683
--- /dev/null
+++ b/kv/src/main/kotlin/org/apache/tuweni/kv/ProxyKeyValueStore.kt
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tuweni.kv
+
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * A store used as a proxy for another store.
+ *
+ * For example, we may want to store rich objects and transform them to a lower-level form,
+ * or reuse the same store across multiple usages.
+ */
+class ProxyKeyValueStore<K, V, E, R>(
+ private val store: KeyValueStore<E, R>,
+ private val unproxyKey: (E) -> K,
+ private val proxyKey: (K) -> E,
+ private val unproxyValue: (R?) -> V?,
+ private val proxyValue: (V) -> R,
+ override val coroutineContext: CoroutineContext = store.coroutineContext
+) : KeyValueStore<K, V> {
+
+ override suspend fun get(key: K): V? = unproxyValue(store.get(proxyKey(key)))
+
+ override suspend fun put(key: K, value: V) = store.put(proxyKey(key), proxyValue(value))
+
+ override suspend fun keys(): Iterable<K> = store.keys().map(unproxyKey)
+
+ override fun close() = store.close()
+}
diff --git a/kv/src/test/java/org/apache/tuweni/kv/Store.java b/kv/src/test/java/org/apache/tuweni/kv/Store.java
new file mode 100644
index 0000000..069db93
--- /dev/null
+++ b/kv/src/test/java/org/apache/tuweni/kv/Store.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
+ * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
+ * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
+ * License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.tuweni.kv;
+
+import java.util.Objects;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+@Entity(name = "STORE")
+public class Store {
+
+ public Store() {}
+
+ public Store(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Id
+ private String key;
+
+ private String value;
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof Store))
+ return false;
+ Store store = (Store) o;
+ return Objects.equals(key, store.key) && Objects.equals(value, store.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, value);
+ }
+}
diff --git a/kv/src/test/kotlin/org/apache/tuweni/kv/KeyValueStoreSpec.kt b/kv/src/test/kotlin/org/apache/tuweni/kv/KeyValueStoreSpec.kt
index 94f4ffb..a69277b 100644
--- a/kv/src/test/kotlin/org/apache/tuweni/kv/KeyValueStoreSpec.kt
+++ b/kv/src/test/kotlin/org/apache/tuweni/kv/KeyValueStoreSpec.kt
@@ -21,6 +21,7 @@
import com.winterbe.expekt.should
import kotlinx.coroutines.runBlocking
import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.io.Base64
import org.apache.tuweni.kv.Vars.bar
import org.apache.tuweni.kv.Vars.foo
import org.apache.tuweni.kv.Vars.foobar
@@ -35,6 +36,7 @@
import java.nio.file.Paths
import java.sql.DriverManager
import java.util.concurrent.RejectedExecutionException
+import javax.persistence.Persistence
object Vars {
val foo = Bytes.wrap("foo".toByteArray())!!
@@ -380,3 +382,100 @@
}
}
})
+
+object EntityManagerKeyValueStoreSpec : Spek({
+ val entityManagerFactory = Persistence.createEntityManagerFactory("h2")
+ val kv = EntityManagerKeyValueStore(entityManagerFactory::createEntityManager, Store::class.java, { it.key })
+ afterGroup {
+ kv.close()
+ }
+ describe("a JPA entity manager-backed key value store") {
+
+ it("should allow to retrieve values") {
+ runBlocking {
+ kv.put("foo", Store("foo", "bar"))
+ kv.get("foo").should.equal(Store("foo", "bar"))
+ }
+ }
+
+ it("should allow to update values") {
+ runBlocking {
+ kv.put("foo", Store("foo", "bar"))
+ kv.put("foo", Store("foo", "foobar"))
+ kv.get("foo").should.equal(Store("foo", "foobar"))
+ }
+ }
+
+ it("should allow to update values without keys") {
+ runBlocking {
+ kv.put(Store("foo2", "bar2"))
+ kv.put(Store("foo2", "foobar2"))
+ kv.get("foo2").should.equal(Store("foo2", "foobar2"))
+ }
+ }
+
+ it("should return null when no value is present") {
+ runBlocking {
+ kv.get("foobar").should.be.`null`
+ }
+ }
+
+ it("should iterate over keys") {
+ runBlocking {
+ kv.put("foo", Store("foo", "bar"))
+ kv.put("bar", Store("bar", "bar"))
+ val keys = kv.keys().map { it }
+ keys.should.contain("bar")
+ keys.should.contain("foo")
+ }
+ }
+ }
+})
+
+object ProxyKeyValueStoreSpec : Spek({
+ val kv = MapKeyValueStore<String, String>()
+ val proxy = ProxyKeyValueStore(
+ kv,
+ Base64::decode,
+ Base64::encode,
+ { if (it == null) { null } else { Base64.decode(it) } },
+ Base64::encode
+ )
+ afterGroup {
+ proxy.close()
+ }
+ describe("a proixy key value store") {
+
+ it("should allow to retrieve values") {
+ runBlocking {
+ proxy.put(foo, bar)
+ proxy.get(foo).should.equal(bar)
+ kv.get(Base64.encode(foo)).should.equal(Base64.encode(bar))
+ }
+ }
+
+ it("should allow to update values") {
+ runBlocking {
+ proxy.put(foo, bar)
+ proxy.put(foo, foobar)
+ proxy.get(foo).should.equal(foobar)
+ }
+ }
+
+ it("should return null when no value is present") {
+ runBlocking {
+ proxy.get(foobar).should.be.`null`
+ }
+ }
+
+ it("should iterate over keys") {
+ runBlocking {
+ proxy.put(foo, bar)
+ proxy.put(bar, foo)
+ val keys = proxy.keys().map { it }
+ keys.should.contain(bar)
+ keys.should.contain(foo)
+ }
+ }
+ }
+})
diff --git a/kv/src/test/resources/META-INF/persistence.xml b/kv/src/test/resources/META-INF/persistence.xml
new file mode 100644
index 0000000..e77da5a
--- /dev/null
+++ b/kv/src/test/resources/META-INF/persistence.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<persistence version="2.1"
+ xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
+
+ <!-- h2 -->
+
+ <persistence-unit name="h2" transaction-type="RESOURCE_LOCAL">
+ <class>org.apache.tuweni.kv.Store</class>
+ <properties>
+ <property name="openjpa.RuntimeUnenhancedClasses" value="supported" />
+ <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema"/>
+ <property name="openjpa.InitializeEagerly" value="true"/>
+ <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
+ <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" />
+ <property name="javax.persistence.jdbc.user" value="sa" />
+ <property name="javax.persistence.jdbc.password" value="" />
+ <property name="show_sql" value="true"/>
+ </properties>
+ </persistence-unit>
+
+</persistence>
\ No newline at end of file