diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 472a7fe..8052b9b 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -2,6 +2,7 @@
* #112 Ensure Bootique-provided version of JCache is used
* #113 Upgrade Cayenne to 4.2.1
+* #114 Support for Cayenne 5.0-M1
## 3.0-M3
diff --git a/bootique-cayenne50-jcache/pom.xml b/bootique-cayenne50-jcache/pom.xml
new file mode 100644
index 0000000..e597df8
--- /dev/null
+++ b/bootique-cayenne50-jcache/pom.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+ 4.0.0
+
+ io.bootique.cayenne
+ bootique-cayenne-parent
+ 3.0-SNAPSHOT
+
+
+ bootique-cayenne50-jcache
+ jar
+
+ bootique-cayenne50-jcache: Integrates bootique-cayenne with bootique-jcache
+ Integrates bootique-cayenne with bootique-jcache.
+
+
+
+
+ io.bootique.cayenne
+ bootique-cayenne50
+ ${bootique.version}
+
+
+ org.apache.cayenne
+ cayenne-jcache
+ ${cayenne50.version}
+
+
+ org.apache.cayenne
+ cayenne-cache-invalidation
+ ${cayenne50.version}
+
+
+
+
+
+
+
+ io.bootique.cayenne
+ bootique-cayenne50
+
+
+ org.apache.cayenne
+ cayenne-jcache
+
+
+ javax.cache
+ cache-api
+
+
+
+
+ io.bootique.jcache
+ bootique-jcache
+
+
+ org.apache.cayenne
+ cayenne-cache-invalidation
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.ehcache
+ ehcache
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+ io.bootique.cayenne
+ bootique-cayenne50-junit5
+ test
+ ${bootique.version}
+
+
+ io.bootique.jdbc
+ bootique-jdbc-junit5-derby
+ test
+ ${bootique.version}
+
+
+
+
+
+
+ gpg
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
diff --git a/bootique-cayenne50-jcache/src/main/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModule.java b/bootique-cayenne50-jcache/src/main/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModule.java
new file mode 100644
index 0000000..5b1cde8
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/main/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModule.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache;
+
+import io.bootique.BQModule;
+import io.bootique.ModuleCrate;
+import io.bootique.cayenne.v50.CayenneModule;
+import io.bootique.di.Binder;
+import io.bootique.di.Key;
+import io.bootique.di.Provides;
+import org.apache.cayenne.cache.invalidation.CacheInvalidationModule;
+import org.apache.cayenne.cache.invalidation.CacheInvalidationModuleExtender;
+import org.apache.cayenne.cache.invalidation.InvalidationHandler;
+
+import javax.cache.CacheManager;
+import javax.inject.Qualifier;
+import javax.inject.Singleton;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Set;
+
+/**
+ * Bootique DI module integrating bootique-jcache to Cayenne.
+ */
+public class CayenneJCacheModule implements BQModule {
+
+ /**
+ * @param binder DI binder passed to the Module that invokes this method.
+ * @return an instance of {@link CayenneJCacheModuleExtender} that can be used to load Cayenne cache
+ * custom extensions.
+ */
+ public static CayenneJCacheModuleExtender extend(Binder binder) {
+ return new CayenneJCacheModuleExtender(binder);
+ }
+
+ @Override
+ public ModuleCrate crate() {
+ return ModuleCrate.of(this)
+ .description("Integrates Apache Cayenne 4.2 JCache extensions")
+ .build();
+ }
+
+ @Override
+ public void configure(Binder binder) {
+ extend(binder).initAllExtensions();
+
+ CayenneModule.extend(binder).addModule(Key.get(org.apache.cayenne.di.Module.class, DefinedInCayenneJCache.class));
+ }
+
+ @Singleton
+ @Provides
+ @DefinedInCayenneJCache
+ org.apache.cayenne.di.Module provideDiJCacheModule(CacheManager cacheManager, Set invalidationHandlers) {
+ // return module composition
+ return b -> {
+ createInvalidationModule(invalidationHandlers).configure(b);
+ createOverridesModule(cacheManager).configure(b);
+ };
+ }
+
+ protected org.apache.cayenne.di.Module createInvalidationModule(Set invalidationHandlers) {
+ return b -> {
+ CacheInvalidationModuleExtender extender = CacheInvalidationModule.extend(b);
+ invalidationHandlers.forEach(extender::addHandler);
+ };
+ }
+
+ protected org.apache.cayenne.di.Module createOverridesModule(CacheManager cacheManager) {
+ return b -> b.bind(CacheManager.class).toInstance(cacheManager);
+ }
+
+ @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ @Qualifier
+ @interface DefinedInCayenneJCache {
+ }
+}
diff --git a/bootique-cayenne50-jcache/src/main/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleExtender.java b/bootique-cayenne50-jcache/src/main/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleExtender.java
new file mode 100644
index 0000000..17a644c
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/main/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleExtender.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache;
+
+import io.bootique.ModuleExtender;
+import io.bootique.di.Binder;
+import io.bootique.di.SetBuilder;
+import io.bootique.jcache.JCacheModule;
+import org.apache.cayenne.cache.invalidation.InvalidationHandler;
+import org.apache.cayenne.jcache.JCacheConstants;
+
+import javax.cache.configuration.Configuration;
+
+public class CayenneJCacheModuleExtender extends ModuleExtender {
+
+ public CayenneJCacheModuleExtender(Binder binder) {
+ super(binder);
+ }
+
+ @Override
+ public CayenneJCacheModuleExtender initAllExtensions() {
+ contributeInvalidationHandler();
+ return this;
+ }
+
+ public CayenneJCacheModuleExtender addInvalidationHandler(InvalidationHandler handler) {
+ contributeInvalidationHandler().addInstance(handler);
+ return this;
+ }
+
+ public CayenneJCacheModuleExtender addInvalidationHandler(Class extends InvalidationHandler> handlerType) {
+ contributeInvalidationHandler().add(handlerType);
+ return this;
+ }
+
+ // TODO: we actually know key and value types for Cayenne QueryCache config
+ public CayenneJCacheModuleExtender setDefaultCacheConfiguration(Configuration, ?> config) {
+ JCacheModule.extend(binder).setConfiguration(JCacheConstants.DEFAULT_CACHE_NAME, config);
+ return this;
+ }
+
+ protected SetBuilder contributeInvalidationHandler() {
+ return newSet(InvalidationHandler.class);
+ }
+}
diff --git a/bootique-cayenne50-jcache/src/main/resources/META-INF/services/io.bootique.BQModule b/bootique-cayenne50-jcache/src/main/resources/META-INF/services/io.bootique.BQModule
new file mode 100644
index 0000000..6de352c
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/main/resources/META-INF/services/io.bootique.BQModule
@@ -0,0 +1 @@
+io.bootique.cayenne.v50.jcache.CayenneJCacheModule
\ No newline at end of file
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleIT.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleIT.java
new file mode 100644
index 0000000..8deee5d
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleIT.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.jcache.persistent.Table1;
+import io.bootique.cayenne.v50.junit5.CayenneTester;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.cache.QueryCache;
+import org.apache.cayenne.jcache.JCacheQueryCache;
+import org.apache.cayenne.query.ObjectSelect;
+import org.junit.jupiter.api.Test;
+
+import javax.cache.CacheManager;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@BQTest
+public class CayenneJCacheModuleIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class)
+ .deleteBeforeEachTest();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime runtime = Bootique.app("-c", "classpath:bq1.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ @Test
+ public void cacheProvider() {
+ QueryCache cache = cayenne.getRuntime().getInjector().getInstance(QueryCache.class);
+ assertTrue(cache instanceof JCacheQueryCache, "Unexpected cache type: " + cache.getClass().getName());
+ }
+
+ @Test
+ public void cacheManager() {
+ CacheManager cacheManager = runtime.getInstance(CacheManager.class);
+ assertTrue(cacheManager.getClass().getName().startsWith("org.ehcache.jsr107"),
+ "Unexpected cache type: " + cacheManager.getClass().getName());
+
+ CacheManager expectedCacheManager = runtime.getInstance(CacheManager.class);
+ assertSame(expectedCacheManager, cacheManager);
+ }
+
+ @Test
+ public void cachedQueries() {
+
+ ObjectContext context = cayenne.getRuntime().newContext();
+ ObjectSelect g1 = ObjectSelect.query(Table1.class).localCache("g1");
+ ObjectSelect g2 = ObjectSelect.query(Table1.class).localCache("g2");
+
+ db.getTable(cayenne.getTableName(Table1.class)).insert(1).insert(45);
+ assertEquals(2, g1.select(context).size());
+
+ // we are still cached, must not see the new changes
+ db.getTable(cayenne.getTableName(Table1.class)).insert(2).insert(44);
+ assertEquals(2, g1.select(context).size());
+
+ // different cache group - must see the changes
+ assertEquals(4, g2.select(context).size());
+
+ // refresh the cache, so that "g1" could see the changes
+ cayenne.getRuntime().getDataDomain().getQueryCache().removeGroup("g1");
+ assertEquals(4, g1.select(context).size());
+ }
+}
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleTest.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleTest.java
new file mode 100644
index 0000000..6857309
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/CayenneJCacheModuleTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache;
+
+import io.bootique.junit5.BQModuleTester;
+import io.bootique.junit5.BQTest;
+import org.junit.jupiter.api.Test;
+
+@BQTest
+public class CayenneJCacheModuleTest {
+
+ @Test
+ public void autoLoadable() {
+ BQModuleTester.of(CayenneJCacheModule.class).testAutoLoadable().testConfig();
+ }
+}
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/invalidation/CacheInvalidationIT.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/invalidation/CacheInvalidationIT.java
new file mode 100644
index 0000000..a3cf39a
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/invalidation/CacheInvalidationIT.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache.invalidation;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.jcache.CayenneJCacheModule;
+import io.bootique.cayenne.v50.jcache.persistent.Table1;
+import io.bootique.cayenne.v50.jcache.persistent.Table2;
+import io.bootique.cayenne.v50.junit5.CayenneTester;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.cache.invalidation.CacheGroupDescriptor;
+import org.apache.cayenne.cache.invalidation.CacheGroups;
+import org.apache.cayenne.cache.invalidation.InvalidationHandler;
+import org.apache.cayenne.query.ObjectSelect;
+import org.junit.jupiter.api.Test;
+
+import javax.cache.Cache;
+import javax.cache.CacheManager;
+
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@BQTest
+public class CacheInvalidationIT {
+ static final InvalidationHandler invalidationHandler =
+ type -> type.getAnnotation(CacheGroups.class) == null
+ ? p -> asList(new CacheGroupDescriptor("cayenne1"), new CacheGroupDescriptor("nocayenne1"))
+ : null;
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class, Table2.class)
+ .deleteBeforeEachTest();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime runtime = Bootique.app("-c", "classpath:bq1.yml")
+ .autoLoadModules()
+ .module(b -> CayenneJCacheModule.extend(b).addInvalidationHandler(invalidationHandler))
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ @Test
+ public void invalidate_CustomHandler() {
+
+ ObjectContext context = cayenne.getRuntime().newContext();
+ // no explicit cache group must still work - it lands inside default cache called 'cayenne.default.cache'
+ ObjectSelect g0 = ObjectSelect.query(Table1.class).localCache();
+ ObjectSelect g1 = ObjectSelect.query(Table1.class).localCache("cayenne1");
+ ObjectSelect g2 = ObjectSelect.query(Table1.class).localCache("cayenne2");
+
+ assertEquals(0, g0.select(context).size());
+ assertEquals(0, g1.select(context).size());
+ assertEquals(0, g2.select(context).size());
+
+ db.getTable(cayenne.getTableName(Table1.class)).insert(1).insert(2);
+
+ // inserted via SQL... query results are still cached...
+ assertEquals(0, g0.select(context).size());
+ assertEquals(0, g1.select(context).size());
+ assertEquals(0, g2.select(context).size());
+
+
+ Table1 t11 = context.newObject(Table1.class);
+ context.commitChanges();
+
+ // inserted via Cayenne... "g1" should get auto refreshed...
+ assertEquals(0, g0.select(context).size());
+ assertEquals(3, g1.select(context).size());
+ assertEquals(0, g2.select(context).size());
+
+
+ context.deleteObject(t11);
+ context.commitChanges();
+
+ // deleted via Cayenne... "g1" should get auto refreshed
+ assertEquals(0, g0.select(context).size());
+ assertEquals(2, g1.select(context).size());
+ assertEquals(0, g2.select(context).size());
+ }
+
+ @Test
+ public void invalidate_CacheGroup() {
+
+ ObjectContext context = cayenne.getRuntime().newContext();
+ ObjectSelect g3 = ObjectSelect.query(Table2.class).localCache("cayenne3");
+ ObjectSelect g4 = ObjectSelect.query(Table2.class).localCache("cayenne4");
+
+ assertEquals(0, g3.select(context).size());
+ assertEquals(0, g4.select(context).size());
+
+ db.getTable(cayenne.getTableName(Table2.class)).insertColumns("id", "name").values(1, "x1").exec();
+
+ // inserted via SQL... query results are still cached...
+ assertEquals(0, g3.select(context).size());
+ assertEquals(0, g3.select(context).size());
+
+ Table2 t21 = context.newObject(Table2.class);
+ context.commitChanges();
+
+ // inserted via Cayenne... "g1" should get auto refreshed...
+ assertEquals(2, g3.select(context).size());
+ assertEquals(0, g4.select(context).size());
+
+ context.deleteObject(t21);
+ context.commitChanges();
+
+ // deleted via Cayenne... "g1" should get auto refreshed
+ assertEquals(1, g3.select(context).size());
+ assertEquals(0, g4.select(context).size());
+ }
+
+ @Test
+ public void invalidate_CustomData() {
+
+ ObjectContext context = cayenne.getRuntime().newContext();
+
+ // make sure Cayenne-specific caches are created...
+ ObjectSelect g1 = ObjectSelect.query(Table1.class).localCache("cayenne1");
+ assertEquals(0, g1.select(context).size());
+
+ // add custom data
+ CacheManager cacheManager = runtime.getInstance(CacheManager.class);
+ Cache cache = cacheManager.getCache("cayenne1");
+ cache.put("a", "b");
+
+ assertEquals("b", cache.get("a"));
+
+ // generate commit event
+ context.newObject(Table1.class);
+ context.commitChanges();
+
+ // custom cache entries must expire
+ assertNull(cache.get("a"));
+ }
+}
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/Table1.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/Table1.java
new file mode 100644
index 0000000..07014cd
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/Table1.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache.persistent;
+
+
+import io.bootique.cayenne.v50.jcache.persistent.auto._Table1;
+
+public class Table1 extends _Table1 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/Table2.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/Table2.java
new file mode 100644
index 0000000..e4f4aec
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/Table2.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.jcache.persistent;
+
+import io.bootique.cayenne.v50.jcache.persistent.auto._Table2;
+import org.apache.cayenne.cache.invalidation.CacheGroups;
+
+@CacheGroups("cayenne3")
+public class Table2 extends _Table2 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/auto/_Table1.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/auto/_Table1.java
new file mode 100644
index 0000000..285e083
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/auto/_Table1.java
@@ -0,0 +1,71 @@
+package io.bootique.cayenne.v50.jcache.persistent.auto;
+
+import io.bootique.cayenne.v50.jcache.persistent.Table1;
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+/**
+ * Class _Table1 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Table1 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(Table1.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+
+
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ }
+
+}
diff --git a/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/auto/_Table2.java b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/auto/_Table2.java
new file mode 100644
index 0000000..bfa365a
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/java/io/bootique/cayenne/v50/jcache/persistent/auto/_Table2.java
@@ -0,0 +1,91 @@
+package io.bootique.cayenne.v50.jcache.persistent.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import io.bootique.cayenne.v50.jcache.persistent.Table2;
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _Table2 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Table2 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(Table2.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final StringProperty NAME = PropertyFactory.createString("name", String.class);
+
+ protected String name;
+
+
+ public void setName(String name) {
+ beforePropertyWrite("name", this.name, name);
+ this.name = name;
+ }
+
+ public String getName() {
+ beforePropertyRead("name");
+ return this.name;
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "name":
+ return this.name;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "name":
+ this.name = (String)val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.name);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.name = (String)in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-jcache/src/test/resources/bq1.yml b/bootique-cayenne50-jcache/src/test/resources/bq1.yml
new file mode 100644
index 0000000..6def5cb
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/resources/bq1.yml
@@ -0,0 +1,18 @@
+# Licensed to ObjectStyle LLC under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ObjectStyle LLC 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.
+
+cayenne:
+ configs:
+ - cayenne-project1.xml
\ No newline at end of file
diff --git a/bootique-cayenne50-jcache/src/test/resources/cayenne-project1.xml b/bootique-cayenne50-jcache/src/test/resources/cayenne-project1.xml
new file mode 100644
index 0000000..8942b06
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/resources/cayenne-project1.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/bootique-cayenne50-jcache/src/test/resources/datamap1.map.xml b/bootique-cayenne50-jcache/src/test/resources/datamap1.map.xml
new file mode 100644
index 0000000..e8f1ff7
--- /dev/null
+++ b/bootique-cayenne50-jcache/src/test/resources/datamap1.map.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TABLE
+ VIEW
+
+ false
+ false
+ org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator
+ false
+ false
+ false
+
+
+ Default
+ ../java
+ entity
+ templates/v4_1/subclass.vm
+ templates/v4_1/superclass.vm
+ templates/v4_1/embeddable-subclass.vm
+ templates/v4_1/embeddable-superclass.vm
+ templates/v4_1/datamap-subclass.vm
+ templates/v4_1/datamap-superclass.vm
+ *.java
+ true
+ true
+ false
+ false
+ false
+
+
diff --git a/bootique-cayenne50-junit5/pom.xml b/bootique-cayenne50-junit5/pom.xml
new file mode 100644
index 0000000..94a0fb7
--- /dev/null
+++ b/bootique-cayenne50-junit5/pom.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+ 4.0.0
+
+ io.bootique.cayenne
+ bootique-cayenne-parent
+ 3.0-SNAPSHOT
+
+
+ bootique-cayenne50-junit5
+ jar
+
+ bootique-cayenne50-junit5: JUnit 5-based helper classes for unit tests with Cayenne stack
+ Provides JUnit 5 based helper classes for unit tests with Cayenne stack
+
+
+
+
+ io.bootique.cayenne
+ bootique-cayenne50
+ ${bootique.version}
+
+
+
+
+
+
+
+ io.bootique.cayenne
+ bootique-cayenne50
+
+
+ io.bootique
+ bootique-junit5
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+ io.bootique.jdbc
+ bootique-jdbc-junit5-derby
+ test
+
+
+
+
+
+
+ gpg
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/CayenneTester.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/CayenneTester.java
new file mode 100644
index 0000000..c9752b0
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/CayenneTester.java
@@ -0,0 +1,305 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQCoreModule;
+import io.bootique.BQModule;
+import io.bootique.cayenne.v50.CayenneModule;
+import io.bootique.cayenne.v50.junit5.tester.*;
+import io.bootique.di.Binder;
+import io.bootique.junit5.BQTestScope;
+import io.bootique.junit5.scope.BQAfterMethodCallback;
+import io.bootique.junit5.scope.BQBeforeMethodCallback;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.exp.property.Property;
+import org.apache.cayenne.exp.property.RelationshipProperty;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * A JUnit5 extension that manages test schema, data and Cayenne runtime state between tests. A single CayenneTester
+ * can be used with a single {@link io.bootique.BQRuntime}. If you have multiple BQRuntimes in a test, you will need to
+ * declare a separate CayenneTester for each one of them.
+ *
+ * @since 2.0
+ */
+public class CayenneTester implements BQBeforeMethodCallback, BQAfterMethodCallback {
+
+ private boolean refreshCayenneCaches;
+ private boolean deleteBeforeEachTest;
+ private boolean skipSchemaCreation;
+ private Collection> entities;
+ private Collection> entityGraphRoots;
+ private boolean allTables;
+ private Collection tables;
+ private Collection tableGraphRoots;
+ private Collection relatedTables;
+
+ private final CayenneTesterLifecycleManager lifecycleManager;
+ private CayenneRuntimeManager runtimeManager;
+ private CommitCounter commitCounter;
+ private QueryCounter queryCounter;
+
+ public static CayenneTester create() {
+ return new CayenneTester();
+ }
+
+ protected CayenneTester() {
+
+ this.lifecycleManager = new CayenneTesterLifecycleManager()
+ .callback(this::createRuntimeManager, io.bootique.cayenne.v50.junit5.tester.CayenneTesterCallbackType.onCayenneStartup)
+ .callback(r -> createSchema(), io.bootique.cayenne.v50.junit5.tester.CayenneTesterCallbackType.onCayenneStartup)
+ .callback(r -> refreshCaches(), io.bootique.cayenne.v50.junit5.tester.CayenneTesterCallbackType.beforeTestOrOnCayenneStartupWithinTest)
+ .callback(r -> deleteData(), io.bootique.cayenne.v50.junit5.tester.CayenneTesterCallbackType.beforeTestOrOnCayenneStartupWithinTest)
+ .callback(r -> commitCounter.reset(), io.bootique.cayenne.v50.junit5.tester.CayenneTesterCallbackType.beforeTestOrOnCayenneStartupWithinTest)
+ .callback(r -> queryCounter.reset(), io.bootique.cayenne.v50.junit5.tester.CayenneTesterCallbackType.beforeTestOrOnCayenneStartupWithinTest);
+
+ this.refreshCayenneCaches = true;
+ this.deleteBeforeEachTest = false;
+ this.skipSchemaCreation = false;
+ this.commitCounter = new CommitCounter();
+ this.queryCounter = new QueryCounter();
+ }
+
+ @Override
+ public void beforeMethod(BQTestScope scope, ExtensionContext context) {
+ lifecycleManager.beforeMethod(scope, context);
+ }
+
+ @Override
+ public void afterMethod(BQTestScope scope, ExtensionContext context) {
+ lifecycleManager.afterMethod(scope, context);
+ }
+
+ /**
+ * @since 2.0
+ */
+ public CayenneTester onInit(Consumer callback) {
+ lifecycleManager.callback(callback, CayenneTesterCallbackType.onCayenneStartup);
+ return this;
+ }
+
+ public CayenneTester doNoRefreshCayenneCaches() {
+ this.refreshCayenneCaches = false;
+ return this;
+ }
+
+ public CayenneTester skipSchemaCreation() {
+ this.skipSchemaCreation = true;
+ return this;
+ }
+
+ /**
+ * @since 2.0
+ */
+ public final CayenneTester allTables() {
+ this.allTables = true;
+ return this;
+ }
+
+ /**
+ * Configures the Tester to manage a subset of entities out a potentially very large number of entities in the model.
+ *
+ * @param entities a list of entities to manage (create schema for, delete test data, etc.)
+ * @return this tester
+ */
+ @SafeVarargs
+ public final CayenneTester entities(Class extends Persistent>... entities) {
+
+ if (this.entities == null) {
+ this.entities = new HashSet<>();
+ }
+
+ Collections.addAll(this.entities, entities);
+ return this;
+ }
+
+ @SafeVarargs
+ public final CayenneTester entitiesAndDependencies(Class extends Persistent>... entities) {
+ if (this.entityGraphRoots == null) {
+ this.entityGraphRoots = new HashSet<>();
+ }
+
+ Collections.addAll(this.entityGraphRoots, entities);
+ return this;
+ }
+
+ public CayenneTester tables(String... tables) {
+
+ if (this.tables == null) {
+ this.tables = new HashSet<>();
+ }
+
+ Collections.addAll(this.tables, tables);
+ return this;
+ }
+
+ public CayenneTester tablesAndDependencies(String... tables) {
+
+ if (this.tableGraphRoots == null) {
+ this.tableGraphRoots = new HashSet<>();
+ }
+
+ Collections.addAll(this.tableGraphRoots, tables);
+ return this;
+ }
+
+ public CayenneTester relatedTables(Class extends Persistent> entityType, Property> relationship) {
+
+ if (this.relatedTables == null) {
+ this.relatedTables = new HashSet<>();
+ }
+
+ this.relatedTables.add(new RelatedEntity(entityType, relationship.getName()));
+ return this;
+ }
+
+ /**
+ * Configures the Tester to delete data corresponding to the tester's entity model before each test.
+ *
+ * @return this tester
+ */
+ public CayenneTester deleteBeforeEachTest() {
+ this.deleteBeforeEachTest = true;
+ return this;
+ }
+
+ /**
+ * Returns a new Bootique module that registers Cayenne test extensions. This module should be passed to a single
+ * test {@link io.bootique.BQRuntime} to associate the tester with that runtime. This would allow the tester to
+ * interact with Cayenne stack inside that runtime to perform its functions through the test lifecycle.
+ *
+ * @return a new Bootique module that registers Cayenne test extensions
+ */
+ public BQModule moduleWithTestHooks() {
+ return this::configure;
+ }
+
+ protected void configure(Binder binder) {
+
+ BQCoreModule.extend(binder)
+ .addRuntimeListener(lifecycleManager);
+
+ CayenneModule.extend(binder)
+ .addStartupListener(lifecycleManager)
+ .addSyncFilter(commitCounter)
+ .addQueryFilter(queryCounter);
+ }
+
+ public CayenneRuntime getRuntime() {
+ return lifecycleManager.getCayenneRuntime();
+ }
+
+ protected CayenneRuntimeManager getRuntimeManager() {
+ Assertions.assertNotNull(runtimeManager, "Cayenne runtime is not resolved. Called outside of test lifecycle?");
+ return runtimeManager;
+ }
+
+ public String getTableName(Class extends Persistent> entity) {
+ ObjEntity e = getRuntime().getDataDomain().getEntityResolver().getObjEntity(entity);
+ if (e == null) {
+ throw new IllegalStateException("Type is not mapped in Cayenne: " + entity);
+ }
+
+ return e.getDbEntity().getName();
+ }
+
+ /**
+ * Returns a name of a table related to a given entity via the specified relationship. Useful for navigation to
+ * join tables that are not directly mapped to Java classes.
+ *
+ * @param entity persistent object type for the source of the relationship
+ * @param relationship a relationship that we'll traverse from the source entity to some target entity
+ * @param tableIndex An index in a list of tables spanned by 'relationship'. Index of 0 corresponds to the target
+ * DbEntity of the first object in a chain of DbRelationships for a given ObjRelationship.
+ * @return a name of a table related to a given entity via the specified relationship.
+ * @since 2.0
+ */
+ public String getRelatedTableName(Class extends Persistent> entity, RelationshipProperty> relationship, int tableIndex) {
+ EntityResolver entityResolver = getRuntime().getDataDomain().getEntityResolver();
+ return new RelatedEntity(entity, relationship.getName()).getRelatedTable(entityResolver, tableIndex).getName();
+ }
+
+ /**
+ * Checks whether Cayenne performed the expected number of DB commits within a single test method.
+ */
+ public void assertCommitCount(int expected) {
+ commitCounter.assertCount(expected);
+ }
+
+ /**
+ * Checks whether Cayenne performed the expected number of DB queries within a single test method.
+ *
+ * @since 2.0
+ */
+ public void assertQueryCount(int expected) {
+ queryCounter.assertCount(expected);
+ }
+
+ protected void createRuntimeManager(CayenneRuntime runtime) {
+
+ Objects.requireNonNull(runtime, "CayenneTester is not attached to a test app. "
+ + "To take advantage of CayenneTester, pass the module "
+ + "produced via 'moduleWithTestHooks' when assembling a test BQRuntime.");
+
+ if (this.allTables) {
+ this.runtimeManager = CayenneRuntimeManager
+ .builder(runtime.getDataDomain())
+ .dbEntities(runtime.getDataDomain().getEntityResolver().getDbEntities())
+ .build();
+ } else {
+
+ this.runtimeManager = CayenneRuntimeManager
+ .builder(runtime.getDataDomain())
+ .entities(entities)
+ .entityGraphRoots(entityGraphRoots)
+ .tables(tables)
+ .tableGraphRoots(tableGraphRoots)
+ .relatedEntities(relatedTables)
+ .build();
+ }
+ }
+
+ protected void createSchema() {
+ if (!skipSchemaCreation) {
+ getRuntimeManager().createSchema();
+ }
+ }
+
+ protected void refreshCaches() {
+ if (refreshCayenneCaches) {
+ getRuntimeManager().refreshCaches();
+ }
+ }
+
+ protected void deleteData() {
+ if (deleteBeforeEachTest) {
+ getRuntimeManager().deleteData();
+ }
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneRuntimeManager.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneRuntimeManager.java
new file mode 100644
index 0000000..81acaa4
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneRuntimeManager.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.DbGenerator;
+import org.apache.cayenne.access.util.DoNothingOperationObserver;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.SQLTemplate;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Manages various aspects of Cayenne stack (caching, schema generation, etc.) for a subset of selected entities.
+ *
+ * @since 2.0
+ */
+public class CayenneRuntimeManager {
+
+ private DataDomain domain;
+ private Map managedEntitiesByNode;
+
+ public static CayenneRuntimeManagerBuilder builder(DataDomain domain) {
+ return new CayenneRuntimeManagerBuilder(domain);
+ }
+
+ protected CayenneRuntimeManager(DataDomain domain, Map managedEntitiesByNode) {
+ this.domain = domain;
+ this.managedEntitiesByNode = managedEntitiesByNode;
+ }
+
+ public void refreshCaches() {
+ if (domain.getSharedSnapshotCache() != null) {
+ domain.getSharedSnapshotCache().clear();
+ }
+
+ if (domain.getQueryCache() != null) {
+ // note that this also flushes per-context caches .. at least with JCache implementation
+ domain.getQueryCache().clear();
+ }
+ }
+
+ // keeping public for the tests
+ public Map getManagedEntitiesByNode() {
+ return managedEntitiesByNode;
+ }
+
+ public void deleteData() {
+ managedEntitiesByNode.forEach(this::deleteData);
+ }
+
+ public void createSchema() {
+ managedEntitiesByNode.forEach(this::createSchema);
+ }
+
+ protected void deleteData(String nodeName, FilteredDataMap map) {
+
+ DataNode node = domain.getDataNode(nodeName);
+
+ // TODO: single transaction?
+ map.getEntitiesInDeleteOrder().forEach(e -> deleteData(node, e));
+ }
+
+ protected void deleteData(DataNode node, DbEntity entity) {
+
+ String name = node.getAdapter().getQuotingStrategy().quotedFullyQualifiedName(entity);
+ String sql = "delete from " + name;
+
+ // using SQLTemplate instead of SQLExec, as it can be executed directly on the DataNode
+ SQLTemplate query = new SQLTemplate();
+ query.setDefaultTemplate(sql);
+ node.performQueries(Collections.singleton(query), new DoNothingOperationObserver());
+ }
+
+ protected void createSchema(String nodeName, DataMap map) {
+
+ DataNode node = domain.getDataNode(nodeName);
+
+ DbGenerator generator = new DbGenerator(node.getAdapter(), map, node.getJdbcEventLogger());
+ generator.setShouldCreateTables(true);
+ generator.setShouldDropTables(false);
+ generator.setShouldCreateFKConstraints(true);
+ generator.setShouldCreatePKSupport(true);
+ generator.setShouldDropPKSupport(false);
+
+ try {
+ generator.runGenerator(node.getDataSource());
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating schema for DataNode: " + node.getName(), e);
+ }
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneRuntimeManagerBuilder.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneRuntimeManagerBuilder.java
new file mode 100644
index 0000000..17b39c3
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneRuntimeManagerBuilder.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.map.*;
+
+import java.util.*;
+
+/**
+ * @since 2.0
+ */
+public class CayenneRuntimeManagerBuilder {
+
+ private final DataDomain domain;
+
+ // these two entity buckets are resolved independently and then intersected
+ private final Set entities;
+ private final Set entityGraphs;
+
+ protected CayenneRuntimeManagerBuilder(DataDomain domain) {
+ this.domain = domain;
+ this.entities = new HashSet<>();
+ this.entityGraphs = new HashSet<>();
+ }
+
+ public CayenneRuntimeManagerBuilder dbEntities(Collection entities) {
+ if (entities != null) {
+ this.entities.addAll(entities);
+ }
+
+ return this;
+ }
+
+ public CayenneRuntimeManagerBuilder entities(Collection> entities) {
+ if (entities != null) {
+ entities.forEach(e -> resolve(this.entities, e));
+ }
+
+ return this;
+ }
+
+ public CayenneRuntimeManagerBuilder entityGraphRoots(Collection> entityGraphRoots) {
+ if (entityGraphRoots != null) {
+ entityGraphRoots.forEach(e -> resolveGraph(this.entityGraphs, e));
+ }
+
+ return this;
+ }
+
+ public CayenneRuntimeManagerBuilder tables(Collection tables) {
+ if (tables != null) {
+ tables.forEach(t -> resolve(this.entities, t));
+ }
+
+ return this;
+ }
+
+ public CayenneRuntimeManagerBuilder tableGraphRoots(Collection tableGraphRoots) {
+ if (tableGraphRoots != null) {
+ tableGraphRoots.forEach(t -> resolveGraph(this.entityGraphs, t));
+ }
+
+ return this;
+ }
+
+ public CayenneRuntimeManagerBuilder relatedEntities(Collection relatedEntities) {
+ if (relatedEntities != null) {
+ relatedEntities.forEach(t -> resolve(this.entities, t));
+ }
+ return this;
+ }
+
+ public CayenneRuntimeManager build() {
+
+ // using LinkedHashMap to preserve insert order of entities
+ Map> byNode = new HashMap<>();
+ buildEntitiesInInsertOrder().forEach(e -> {
+ DataNode node = domain.lookupDataNode(e.getDataMap());
+ byNode.computeIfAbsent(node.getName(), nn -> new LinkedHashMap<>()).put(e.getName(), e);
+ });
+
+ Map managedEntitiesByNode = new HashMap<>();
+ byNode.forEach((k, v) -> managedEntitiesByNode.put(k, new FilteredDataMap("CayenneTester_" + k, v)));
+ return new CayenneRuntimeManager(domain, managedEntitiesByNode);
+ }
+
+ private Collection buildEntitiesInInsertOrder() {
+
+ Set dbEntities;
+
+ if (entities.isEmpty() && entityGraphs.isEmpty()) {
+ dbEntities = Collections.emptySet();
+ } else if (entities.isEmpty()) {
+ dbEntities = entityGraphs;
+ } else if (entityGraphs.isEmpty()) {
+ dbEntities = entities;
+ } else {
+ dbEntities = new HashSet<>();
+ dbEntities.addAll(entities);
+ dbEntities.addAll(entityGraphs);
+ }
+
+ // Sort if needed
+ if (dbEntities.size() <= 1) {
+ return dbEntities;
+ }
+
+ // Do not obtain sorter from Cayenne DI. It is not a singleton and will come uninitialized
+ EntitySorter sorter = domain.getEntitySorter();
+ List sorted = new ArrayList<>(dbEntities);
+ sorter.sortDbEntities(sorted, false);
+ return sorted;
+ }
+
+ private void resolve(Set accum, String tableName) {
+ accum.add(dbEntityForName(tableName));
+ }
+
+ private void resolveGraph(Set accum, String tableName) {
+ DbEntity entity = dbEntityForName(tableName);
+ ModelDependencyResolver.resolve(accum, entity);
+ }
+
+ private void resolve(Set accum, Class extends Persistent> type) {
+ accum.add(dbEntityForType(type));
+ }
+
+ private void resolveGraph(Set accum, Class extends Persistent> type) {
+ DbEntity entity = dbEntityForType(type);
+ ModelDependencyResolver.resolve(accum, entity);
+ }
+
+ private void resolve(Set accum, RelatedEntity re) {
+
+ ObjEntity e = domain.getEntityResolver().getObjEntity(re.getType());
+ if (e == null) {
+ throw new IllegalStateException("Type is not mapped in Cayenne: " + re.getType());
+ }
+
+ ObjRelationship objRelationship = e.getRelationship(re.getRelationship());
+ if (objRelationship == null) {
+ throw new IllegalArgumentException("No relationship '" + re.getRelationship() + "' in entity " + e.getName());
+ }
+
+ List path = objRelationship.getDbRelationships();
+ if (path.size() < 2) {
+ return;
+ }
+
+ path.subList(1, path.size())
+ .stream()
+ .map(DbRelationship::getSourceEntity)
+ .forEach(accum::add);
+ }
+
+ private DbEntity dbEntityForType(Class extends Persistent> type) {
+ ObjEntity e = domain.getEntityResolver().getObjEntity(type);
+ if (e == null) {
+ throw new IllegalStateException("Type is not mapped in Cayenne: " + type);
+ }
+
+ return e.getDbEntity();
+ }
+
+ private DbEntity dbEntityForName(String name) {
+ DbEntity dbe = domain.getEntityResolver().getDbEntity(name);
+ if (dbe == null) {
+ throw new IllegalStateException("Table is not mapped in Cayenne: " + name);
+ }
+
+ return dbe;
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneTesterCallbackType.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneTesterCallbackType.java
new file mode 100644
index 0000000..043b2c3
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneTesterCallbackType.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+public enum CayenneTesterCallbackType {
+
+ // run when Cayenne runtime is started
+ onCayenneStartup,
+
+ // run either before each test (when Cayenne is already started) or when Cayenne is started within a test
+ beforeTestOrOnCayenneStartupWithinTest;
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneTesterLifecycleManager.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneTesterLifecycleManager.java
new file mode 100644
index 0000000..c7652c8
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CayenneTesterLifecycleManager.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import io.bootique.BQRuntime;
+import io.bootique.BQRuntimeListener;
+import io.bootique.cayenne.v50.CayenneStartupListener;
+import io.bootique.di.DIRuntimeException;
+import io.bootique.junit5.BQTestScope;
+import io.bootique.junit5.scope.BQAfterMethodCallback;
+import io.bootique.junit5.scope.BQBeforeMethodCallback;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * @since 3.0
+ */
+public class CayenneTesterLifecycleManager implements BQRuntimeListener, CayenneStartupListener, BQBeforeMethodCallback, BQAfterMethodCallback {
+
+ private final Map, CayenneTesterCallbackType> callbacks;
+ private BQRuntime bqRuntime;
+ private CayenneRuntime cayenneRuntime;
+ private boolean withinTestMethod;
+
+ public CayenneTesterLifecycleManager() {
+ // ordering of entries is important here
+ this.callbacks = new LinkedHashMap<>();
+ }
+
+ public BQRuntime getBqRuntime() {
+ assertNotNull(bqRuntime, "BQRuntime is not initialized. Not connected to a Bootique runtime?");
+ return bqRuntime;
+ }
+
+ public CayenneRuntime getCayenneRuntime() {
+
+ if (cayenneRuntime == null) {
+ // trigger immediate initialization
+ getBqRuntime().getInstance(CayenneRuntime.class);
+ }
+
+ assertNotNull(cayenneRuntime, "Unexpected state - CayenneRuntime is not initialized");
+ return cayenneRuntime;
+ }
+
+ /**
+ * Registers a Cayenne startup callback to be run either unconditionally, or only when startup happens within a
+ * test method.
+ */
+ public CayenneTesterLifecycleManager callback(Consumer callback, CayenneTesterCallbackType type) {
+ callbacks.put(callback, type);
+ return this;
+ }
+
+ // Called by "bootique"
+ @Override
+ public void onRuntimeCreated(BQRuntime runtime) {
+ checkUnused(runtime);
+ this.bqRuntime = runtime;
+ }
+
+ // Called by "bootique-cayenne"
+ @Override
+ public void onRuntimeCreated(CayenneRuntime runtime) {
+ this.cayenneRuntime = runtime;
+ callbacks.forEach((k, v) -> onCayenneStarted(runtime, k, v));
+ }
+
+ // Called by "bootique-junit5" via CayenneTester
+ @Override
+ public void beforeMethod(BQTestScope scope, ExtensionContext context) {
+ this.withinTestMethod = true;
+
+ // TODO: prefilter callbacks collection of "beforeTestIfStarted" in a separate collection to avoid iteration
+ // on every run?
+ if (isStarted()) {
+ callbacks.forEach((k, v) -> beforeTestIfStarted(cayenneRuntime, k, v));
+ }
+ }
+
+ // Called by "bootique-junit5" via CayenneTester
+ @Override
+ public void afterMethod(BQTestScope scope, ExtensionContext context) {
+ this.withinTestMethod = false;
+ }
+
+ private boolean isStarted() {
+ return cayenneRuntime != null;
+ }
+
+ private void onCayenneStarted(CayenneRuntime runtime, Consumer callback, CayenneTesterCallbackType type) {
+
+ switch (type) {
+ case onCayenneStartup:
+ callback.accept(runtime);
+ break;
+ case beforeTestOrOnCayenneStartupWithinTest:
+ if (withinTestMethod) {
+ callback.accept(runtime);
+ }
+ break;
+ default:
+ return;
+ }
+ }
+
+ private void beforeTestIfStarted(CayenneRuntime runtime, Consumer callback, CayenneTesterCallbackType type) {
+ if (type == CayenneTesterCallbackType.beforeTestOrOnCayenneStartupWithinTest) {
+ callback.accept(runtime);
+ }
+ }
+
+ private void checkUnused(BQRuntime runtime) {
+ if (this.bqRuntime != null && this.bqRuntime != runtime) {
+ throw new DIRuntimeException("BQRuntime is already initialized. " +
+ "Likely this CayenneTester is already connected to another BQRuntime. " +
+ "To fix this error use one CayenneTester per BQRuntime.");
+ }
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CommitCounter.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CommitCounter.java
new file mode 100644
index 0000000..a936f80
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/CommitCounter.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.DataChannelSyncFilter;
+import org.apache.cayenne.DataChannelSyncFilterChain;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.graph.GraphDiff;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @since 2.0
+ */
+public class CommitCounter implements DataChannelSyncFilter {
+
+ private AtomicInteger count;
+
+ public CommitCounter() {
+ this.count = new AtomicInteger(0);
+ }
+
+ @Override
+ public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType, DataChannelSyncFilterChain filterChain) {
+ count.incrementAndGet();
+ return filterChain.onSync(originatingContext, changes, syncType);
+ }
+
+ public void assertCount(int expectedCommits) {
+ assertEquals(expectedCommits, count.get(), "Unexpected number of Cayenne commits executed");
+ }
+
+ public void reset() {
+ count.set(0);
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/FilteredDataMap.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/FilteredDataMap.java
new file mode 100644
index 0000000..01d36a1
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/FilteredDataMap.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+
+import java.util.*;
+
+/**
+ * A DataMap decorator that provides access to a subset of DbEntities from another DataMap without changing their
+ * parent. Unfortunately it is not easy tio just copy some entities from one DataMap to another, as the entities'
+ * parent will get reset. Hence using thsi decorator.
+ *
+ * @since 2.0
+ */
+public class FilteredDataMap extends DataMap {
+
+ // expected to be in the insert order
+ private LinkedHashMap orderedEntities;
+ private List entitiesInDeleteOrder;
+
+ public FilteredDataMap(String mapName, LinkedHashMap orderedEntities) {
+ super(mapName);
+ this.orderedEntities = orderedEntities;
+ }
+
+ public List getEntitiesInDeleteOrder() {
+ if (entitiesInDeleteOrder == null) {
+ List list = new ArrayList<>(orderedEntities.values());
+ Collections.reverse(list);
+ this.entitiesInDeleteOrder = list;
+ }
+ return entitiesInDeleteOrder;
+ }
+
+ public Collection getDbEntitiesInInsertOrder() {
+ return orderedEntities.values();
+ }
+
+ @Override
+ public SortedMap getDbEntityMap() {
+ return new TreeMap<>(orderedEntities);
+ }
+
+ @Override
+ public Collection getDbEntities() {
+ return getDbEntitiesInInsertOrder();
+ }
+
+ @Override
+ public DbEntity getDbEntity(String dbEntityName) {
+ return orderedEntities.get(dbEntityName);
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/ModelDependencyResolver.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/ModelDependencyResolver.java
new file mode 100644
index 0000000..66e3541
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/ModelDependencyResolver.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+
+import java.util.Set;
+
+class ModelDependencyResolver {
+
+ static void resolve(Set resolved, DbEntity entity) {
+
+ // if already there, assume entity's dependencies are already resolved
+ if (resolved.add(entity)) {
+ entity.getRelationships().forEach(r -> resolveDependent(resolved, r));
+ }
+ }
+
+ static private void resolveDependent(Set resolved, DbRelationship relationship) {
+ if (relationship.isFromPK() && !relationship.isToMasterPK()) {
+ resolve(resolved, relationship.getTargetEntity());
+ }
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/QueryCounter.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/QueryCounter.java
new file mode 100644
index 0000000..4620c62
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/QueryCounter.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.DataChannelQueryFilter;
+import org.apache.cayenne.DataChannelQueryFilterChain;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.query.Query;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @since 2.0
+ */
+public class QueryCounter implements DataChannelQueryFilter {
+
+ private AtomicInteger count;
+
+ public QueryCounter() {
+ this.count = new AtomicInteger(0);
+ }
+
+ @Override
+ public QueryResponse onQuery(ObjectContext objectContext, Query query, DataChannelQueryFilterChain dataChannelQueryFilterChain) {
+ count.incrementAndGet();
+ return dataChannelQueryFilterChain.onQuery(objectContext, query);
+ }
+
+ public void assertCount(int expectedCommits) {
+ assertEquals(expectedCommits, count.get(), "Unexpected number of Cayenne queries executed");
+ }
+
+ public void reset() {
+ count.set(0);
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/RelatedEntity.java b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/RelatedEntity.java
new file mode 100644
index 0000000..6749526
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/main/java/io/bootique/cayenne/v50/junit5/tester/RelatedEntity.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5.tester;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.map.*;
+
+import java.util.List;
+
+/**
+ * @since 2.0
+ */
+public class RelatedEntity {
+
+ private Class extends Persistent> type;
+ private String relationship;
+
+ public RelatedEntity(Class extends Persistent> type, String relationship) {
+ this.type = type;
+ this.relationship = relationship;
+ }
+
+ public Class extends Persistent> getType() {
+ return type;
+ }
+
+ public String getRelationship() {
+ return relationship;
+ }
+
+ /**
+ * Returns a DbEntity related to a given entity via the specified relationship. Useful for navigation to join tables
+ * that are not directly mapped to Java classes.
+ *
+ * @param tableIndex An index in a list of tables spanned by 'relationship'. Index of 0 corresponds to the target
+ * DbEntity of the first object in a chain of DbRelationships for a given ObjRelationship.
+ * @return a DbEntity related to a given entity via the specified relationship.
+ */
+ public DbEntity getRelatedTable(EntityResolver resolver, int tableIndex) {
+ ObjEntity entity = resolver.getObjEntity(type);
+ if (entity == null) {
+ throw new IllegalArgumentException("Not a Cayenne entity class: " + type.getName());
+ }
+
+ ObjRelationship flattened = entity.getRelationship(relationship);
+
+ if (flattened == null) {
+ throw new IllegalArgumentException("No relationship '" + relationship + "' in entity " + type.getName());
+ }
+
+ List path = flattened.getDbRelationships();
+
+ if (path.size() < tableIndex + 1) {
+ throw new IllegalArgumentException("Index " + tableIndex + " is out of bounds for relationship '" + relationship);
+ }
+
+ return path.get(tableIndex).getTargetEntity();
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_AllTablesIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_AllTablesIT.java
new file mode 100644
index 0000000..d926156
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_AllTablesIT.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.tester.FilteredDataMap;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@BQTest
+public class CayenneTester_AllTablesIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .createRuntime();
+
+ private Set getTables(CayenneTester ct) {
+ ct.createRuntimeManager(app.getInstance(CayenneRuntime.class));
+ Map entities = ct.getRuntimeManager().getManagedEntitiesByNode();
+
+ assertEquals(1, entities.size());
+ FilteredDataMap map = entities.values().iterator().next();
+
+ return map.getDbEntitiesInInsertOrder()
+ .stream()
+ .map(DbEntity::getName)
+ .collect(Collectors.toSet());
+ }
+
+ @Test
+ public void allTables() {
+
+ CayenneTester ct = CayenneTester.create().allTables();
+ Set tables = getTables(ct);
+
+ assertEquals(2, tables.size());
+ assertTrue(tables.contains("table1"));
+ assertTrue(tables.contains("table2"));
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_AssertOpCountsIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_AssertOpCountsIT.java
new file mode 100644
index 0000000..3230936
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_AssertOpCountsIT.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.cayenne.v50.junit5.persistence.Table2;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.query.ObjectSelect;
+import org.junit.jupiter.api.RepeatedTest;
+
+@BQTest
+public class CayenneTester_AssertOpCountsIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class, Table2.class)
+ .deleteBeforeEachTest();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ @RepeatedTest(3)
+ public void commitCount() {
+
+ // must be reset at every run
+ cayenne.assertCommitCount(0);
+ ObjectContext context = cayenne.getRuntime().newContext();
+
+ Table1 t1 = context.newObject(Table1.class);
+ t1.setA(7L);
+ t1.setB(8L);
+ context.commitChanges();
+
+ cayenne.assertCommitCount(1);
+ }
+
+ @RepeatedTest(3)
+ public void queryCount() {
+
+ // must be reset at every run
+ cayenne.assertQueryCount(0);
+ ObjectContext context = cayenne.getRuntime().newContext();
+
+ ObjectSelect.query(Table1.class).select(context);
+ cayenne.assertQueryCount(1);
+
+ ObjectSelect.query(Table1.class).select(context);
+ cayenne.assertQueryCount(2);
+
+ // reset context, same counter should be in use
+ ObjectSelect.query(Table1.class).select(cayenne.getRuntime().newContext());
+ cayenne.assertQueryCount(3);
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_CachesNotRefreshedIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_CachesNotRefreshedIT.java
new file mode 100644
index 0000000..ce515b2
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_CachesNotRefreshedIT.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.cayenne.v50.junit5.persistence.Table2;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.junit.jupiter.api.*;
+
+@BQTest
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class CayenneTester_CachesNotRefreshedIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .doNoRefreshCayenneCaches()
+ .entities(Table1.class, Table2.class);
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique.app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ @Test
+ @Order(1)
+ public void crossTestInterference1() {
+ verifyCachesEmptyAndAddObjectsToCache();
+ }
+
+ @Test
+ @Order(2)
+ public void crossTestInterference2() {
+ verifyCaches(1);
+ }
+
+ private void verifyCaches(int expectedCount) {
+ Assertions.assertEquals(expectedCount, cayenne.getRuntime().getDataDomain().getSharedSnapshotCache().size());
+ }
+
+ private void verifyCachesEmptyAndAddObjectsToCache() {
+ // verify that there's no data in the cache
+ verifyCaches(0);
+
+ // seed the cache for the next test
+ ObjectContext context = cayenne.getRuntime().newContext();
+ Table1 t1 = context.newObject(Table1.class);
+ t1.setA(5L);
+ t1.setB(6L);
+ context.commitChanges();
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_CachesRefreshedIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_CachesRefreshedIT.java
new file mode 100644
index 0000000..191ffd7
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_CachesRefreshedIT.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.cayenne.v50.junit5.persistence.Table2;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.junit.jupiter.api.*;
+
+@BQTest
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class CayenneTester_CachesRefreshedIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class, Table2.class);
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique.app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ @Test
+ @Order(1)
+ public void crossTestInterference1() {
+ verifyCachesEmptyAndAddObjectsToCache();
+ }
+
+ @Test
+ @Order(2)
+ public void crossTestInterference2() {
+ verifyCachesEmpty();
+ }
+
+ private void verifyCachesEmpty() {
+ // verify that there's no data in the cache
+ Assertions.assertEquals(0, cayenne.getRuntime().getDataDomain().getSharedSnapshotCache().size());
+ }
+
+ private void verifyCachesEmptyAndAddObjectsToCache() {
+ // verify that there's no data in the cache
+ verifyCachesEmpty();
+
+ // seed the cache for the next test
+ ObjectContext context = cayenne.getRuntime().newContext();
+ Table1 t1 = context.newObject(Table1.class);
+ t1.setA(5L);
+ t1.setB(6L);
+ context.commitChanges();
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_DeleteBeforeEachTestIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_DeleteBeforeEachTestIT.java
new file mode 100644
index 0000000..5824d3b
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_DeleteBeforeEachTestIT.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.cayenne.v50.junit5.persistence.Table2;
+import io.bootique.jdbc.junit5.Table;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.Persistent;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@BQTest
+public class CayenneTester_DeleteBeforeEachTestIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class, Table2.class)
+ .deleteBeforeEachTest();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ @Test
+ public void noSuchTable() {
+ assertThrows(IllegalStateException.class, () -> cayenne.getTableName(Persistent.class));
+ }
+
+ @RepeatedTest(2)
+ public void test1() {
+
+ Table t1 = db.getTable(cayenne.getTableName(Table1.class));
+ Table t2 = db.getTable(cayenne.getTableName(Table2.class));
+
+ t1.matcher().assertNoMatches();
+ t2.matcher().assertNoMatches();
+
+ t1.insert(1, 2, 3);
+ t2.insert(5, "x");
+
+ t1.matcher().assertOneMatch();
+ t2.matcher().assertOneMatch();
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_DependenciesIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_DependenciesIT.java
new file mode 100644
index 0000000..68e0024
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_DependenciesIT.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T1;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T3;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T4;
+import io.bootique.cayenne.v50.junit5.tester.FilteredDataMap;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@BQTest
+public class CayenneTester_DependenciesIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config3.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .createRuntime();
+
+ private Set getTables(CayenneTester ct) {
+ ct.createRuntimeManager(app.getInstance(CayenneRuntime.class));
+ Map entities = ct.getRuntimeManager().getManagedEntitiesByNode();
+
+ assertEquals(1, entities.size());
+ FilteredDataMap map = entities.values().iterator().next();
+
+ return map.getDbEntitiesInInsertOrder()
+ .stream()
+ .map(DbEntity::getName)
+ .collect(Collectors.toSet());
+ }
+
+ @Test
+ public void dependentEntities1() {
+
+ CayenneTester ct = CayenneTester.create().entitiesAndDependencies(P3T1.class);
+ Set tables = getTables(ct);
+
+ assertEquals(4, tables.size());
+ assertTrue(tables.contains("p3_t1"));
+ assertTrue(tables.contains("p3_t1_t4"));
+ assertTrue(tables.contains("p3_t2"));
+ assertTrue(tables.contains("p3_t3"));
+ }
+
+ @Test
+ public void dependentEntities2() {
+ CayenneTester ct = CayenneTester.create().entitiesAndDependencies(P3T4.class);
+ Set tables = getTables(ct);
+
+ assertEquals(2, tables.size());
+ assertTrue(tables.contains("p3_t4"));
+ assertTrue(tables.contains("p3_t1_t4"));
+ }
+
+ @Test
+ public void dependentEntities3() {
+ CayenneTester ct = CayenneTester.create().entitiesAndDependencies(P3T3.class);
+ Set tables = getTables(ct);
+
+ assertEquals(1, tables.size());
+ assertTrue(tables.contains("p3_t3"));
+ }
+
+ @Test
+ public void dependentTables1() {
+ CayenneTester ct = CayenneTester.create().tablesAndDependencies("p3_t1_t4");
+ Set tables = getTables(ct);
+
+ assertEquals(1, tables.size());
+ assertTrue(tables.contains("p3_t1_t4"));
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_IndirectRuntimeAccessIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_IndirectRuntimeAccessIT.java
new file mode 100644
index 0000000..c43bdf9
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_IndirectRuntimeAccessIT.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.RepeatedTest;
+
+@BQTest
+public class CayenneTester_IndirectRuntimeAccessIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class)
+ .deleteBeforeEachTest();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+
+ @RepeatedTest(2)
+ @DisplayName("Eager init of BQRuntime must work without calling 'CayenneTester.getRuntime()'")
+ public void dbAccess() {
+
+ // schema is only created after the first access to CayenneRuntime
+ app.getInstance(CayenneRuntime.class);
+ db.getTable("table1").matcher().assertNoMatches();
+
+ ObjectContext context = app.getInstance(CayenneRuntime.class).newContext();
+ Table1 t1 = context.newObject(Table1.class);
+ t1.setA(5L);
+ t1.setB(6L);
+
+ context.commitChanges();
+
+ db.getTable("table1").matcher().assertMatches(1);
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_ModuleWithTestHooksIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_ModuleWithTestHooksIT.java
new file mode 100644
index 0000000..b26f469
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_ModuleWithTestHooksIT.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.di.DIRuntimeException;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestFactory;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@BQTest
+public class CayenneTester_ModuleWithTestHooksIT {
+
+ @BQTestTool
+ static final BQTestFactory testFactory = new BQTestFactory();
+
+ @Test
+ @DisplayName("Tester should work with BQTestFactory-produced runtimes")
+ public void withBQTestFactory() {
+ CayenneTester tester = CayenneTester.create();
+ BQRuntime runtime = testFactory.app().autoLoadModules().module(tester.moduleWithTestHooks()).createRuntime();
+ CayenneRuntime cayenneRuntime = runtime.getInstance(CayenneRuntime.class);
+ assertSame(cayenneRuntime, tester.getRuntime());
+ }
+
+ @Test
+ @DisplayName("Reusing tester for multiple runtimes must be disallowed")
+ public void disallowMultipleRuntimes() {
+ CayenneTester tester = CayenneTester.create();
+ testFactory.app().autoLoadModules().module(tester.moduleWithTestHooks()).createRuntime();
+
+ assertThrows(DIRuntimeException.class, () ->
+ testFactory.app().autoLoadModules().module(tester.moduleWithTestHooks()).createRuntime());
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_RunAppIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_RunAppIT.java
new file mode 100644
index 0000000..e875a14
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_RunAppIT.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQCoreModule;
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.cli.Cli;
+import io.bootique.command.CommandOutcome;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.ObjectContext;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+@BQTest
+public class CayenneTester_RunAppIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class)
+ .deleteBeforeEachTest();
+
+ @BQApp
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(b -> BQCoreModule.extend(b).setDefaultCommand(CayenneTester_RunAppIT::triggerCayenneInit))
+ .module(db.moduleWithTestDataSource("db"))
+ .module(cayenne.moduleWithTestHooks())
+ .createRuntime();
+
+ private static CommandOutcome triggerCayenneInit(Cli cli) {
+ cayenne.getRuntime();
+ return CommandOutcome.succeeded();
+ }
+
+ @Test
+ @DisplayName("Eager init of BQRuntime must work")
+ public void dbAccess() {
+ ObjectContext context = cayenne.getRuntime().newContext();
+ Table1 t1 = context.newObject(Table1.class);
+ t1.setA(5L);
+ t1.setB(6L);
+
+ context.commitChanges();
+
+ db.getTable("table1").matcher().assertMatches(1);
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_UnattachedIT.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_UnattachedIT.java
new file mode 100644
index 0000000..708e416
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/CayenneTester_UnattachedIT.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50.junit5;
+
+import io.bootique.BQRuntime;
+import io.bootique.Bootique;
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+import io.bootique.jdbc.junit5.derby.DerbyTester;
+import io.bootique.junit5.BQApp;
+import io.bootique.junit5.BQTest;
+import io.bootique.junit5.BQTestTool;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+@BQTest
+public class CayenneTester_UnattachedIT {
+
+ @BQTestTool
+ static final DerbyTester db = DerbyTester.db();
+
+ // declare CayenneTester, but don't use it in the app. While wasteful, this should not blow up
+ @BQTestTool
+ static final CayenneTester cayenne = CayenneTester
+ .create()
+ .entities(Table1.class)
+ .deleteBeforeEachTest();
+
+ @BQApp(skipRun = true)
+ static final BQRuntime app = Bootique
+ .app("-c", "classpath:config2.yml")
+ .autoLoadModules()
+ .module(db.moduleWithTestDataSource("db"))
+ .createRuntime();
+
+ @Test
+ @DisplayName("CayenneTester declared, but not attached to the app")
+ public void dbAccess() {
+ app.getInstance(CayenneRuntime.class).newContext();
+ }
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/Table1.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/Table1.java
new file mode 100644
index 0000000..3187afd
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/Table1.java
@@ -0,0 +1,9 @@
+package io.bootique.cayenne.v50.junit5.persistence;
+
+import io.bootique.cayenne.v50.junit5.persistence.auto._Table1;
+
+public class Table1 extends _Table1 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/Table2.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/Table2.java
new file mode 100644
index 0000000..c7cbd2a
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/Table2.java
@@ -0,0 +1,9 @@
+package io.bootique.cayenne.v50.junit5.persistence;
+
+import io.bootique.cayenne.v50.junit5.persistence.auto._Table2;
+
+public class Table2 extends _Table2 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/auto/_Table1.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/auto/_Table1.java
new file mode 100644
index 0000000..0cd37bd
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/auto/_Table1.java
@@ -0,0 +1,111 @@
+package io.bootique.cayenne.v50.junit5.persistence.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.NumericProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+
+import io.bootique.cayenne.v50.junit5.persistence.Table1;
+
+/**
+ * Class _Table1 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Table1 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(Table1.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final NumericProperty A = PropertyFactory.createNumeric("a", Long.class);
+ public static final NumericProperty B = PropertyFactory.createNumeric("b", Long.class);
+
+ protected Long a;
+ protected Long b;
+
+
+ public void setA(Long a) {
+ beforePropertyWrite("a", this.a, a);
+ this.a = a;
+ }
+
+ public Long getA() {
+ beforePropertyRead("a");
+ return this.a;
+ }
+
+ public void setB(Long b) {
+ beforePropertyWrite("b", this.b, b);
+ this.b = b;
+ }
+
+ public Long getB() {
+ beforePropertyRead("b");
+ return this.b;
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "a":
+ return this.a;
+ case "b":
+ return this.b;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "a":
+ this.a = (Long)val;
+ break;
+ case "b":
+ this.b = (Long)val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.a);
+ out.writeObject(this.b);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.a = (Long)in.readObject();
+ this.b = (Long)in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/auto/_Table2.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/auto/_Table2.java
new file mode 100644
index 0000000..3663ee5
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence/auto/_Table2.java
@@ -0,0 +1,92 @@
+package io.bootique.cayenne.v50.junit5.persistence.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.exp.property.StringProperty;
+
+import io.bootique.cayenne.v50.junit5.persistence.Table2;
+
+/**
+ * Class _Table2 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Table2 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(Table2.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final StringProperty NAME = PropertyFactory.createString("name", String.class);
+
+ protected String name;
+
+
+ public void setName(String name) {
+ beforePropertyWrite("name", this.name, name);
+ this.name = name;
+ }
+
+ public String getName() {
+ beforePropertyRead("name");
+ return this.name;
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "name":
+ return this.name;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "name":
+ this.name = (String)val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.name);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.name = (String)in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T1.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T1.java
new file mode 100644
index 0000000..4aecae1
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T1.java
@@ -0,0 +1,9 @@
+package io.bootique.cayenne.v50.junit5.persistence3;
+
+import io.bootique.cayenne.v50.junit5.persistence3.auto._P3T1;
+
+public class P3T1 extends _P3T1 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T2.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T2.java
new file mode 100644
index 0000000..66dd402
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T2.java
@@ -0,0 +1,9 @@
+package io.bootique.cayenne.v50.junit5.persistence3;
+
+import io.bootique.cayenne.v50.junit5.persistence3.auto._P3T2;
+
+public class P3T2 extends _P3T2 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T3.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T3.java
new file mode 100644
index 0000000..42cee55
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T3.java
@@ -0,0 +1,9 @@
+package io.bootique.cayenne.v50.junit5.persistence3;
+
+import io.bootique.cayenne.v50.junit5.persistence3.auto._P3T3;
+
+public class P3T3 extends _P3T3 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T4.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T4.java
new file mode 100644
index 0000000..1226fc7
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/P3T4.java
@@ -0,0 +1,9 @@
+package io.bootique.cayenne.v50.junit5.persistence3;
+
+import io.bootique.cayenne.v50.junit5.persistence3.auto._P3T4;
+
+public class P3T4 extends _P3T4 {
+
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T1.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T1.java
new file mode 100644
index 0000000..dd60e6f
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T1.java
@@ -0,0 +1,139 @@
+package io.bootique.cayenne.v50.junit5.persistence3.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+
+import io.bootique.cayenne.v50.junit5.persistence3.P3T1;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T2;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T3;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T4;
+
+/**
+ * Class _P3T1 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _P3T1 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(P3T1.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final ListProperty T2S = PropertyFactory.createList("t2s", P3T2.class);
+ public static final EntityProperty T3 = PropertyFactory.createEntity("t3", P3T3.class);
+ public static final ListProperty T4S = PropertyFactory.createList("t4s", P3T4.class);
+
+
+ protected Object t2s;
+ protected Object t3;
+ protected Object t4s;
+
+ public void addToT2s(P3T2 obj) {
+ addToManyTarget("t2s", obj, true);
+ }
+
+ public void removeFromT2s(P3T2 obj) {
+ removeToManyTarget("t2s", obj, true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public List getT2s() {
+ return (List)readProperty("t2s");
+ }
+
+ public void setT3(P3T3 t3) {
+ setToOneTarget("t3", t3, true);
+ }
+
+ public P3T3 getT3() {
+ return (P3T3)readProperty("t3");
+ }
+
+ public void addToT4s(P3T4 obj) {
+ addToManyTarget("t4s", obj, true);
+ }
+
+ public void removeFromT4s(P3T4 obj) {
+ removeToManyTarget("t4s", obj, true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public List getT4s() {
+ return (List)readProperty("t4s");
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "t2s":
+ return this.t2s;
+ case "t3":
+ return this.t3;
+ case "t4s":
+ return this.t4s;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "t2s":
+ this.t2s = val;
+ break;
+ case "t3":
+ this.t3 = val;
+ break;
+ case "t4s":
+ this.t4s = val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.t2s);
+ out.writeObject(this.t3);
+ out.writeObject(this.t4s);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.t2s = in.readObject();
+ this.t3 = in.readObject();
+ this.t4s = in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T2.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T2.java
new file mode 100644
index 0000000..3e493eb
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T2.java
@@ -0,0 +1,91 @@
+package io.bootique.cayenne.v50.junit5.persistence3.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+
+import io.bootique.cayenne.v50.junit5.persistence3.P3T1;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T2;
+
+/**
+ * Class _P3T2 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _P3T2 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(P3T2.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final EntityProperty T1 = PropertyFactory.createEntity("t1", P3T1.class);
+
+
+ protected Object t1;
+
+ public void setT1(P3T1 t1) {
+ setToOneTarget("t1", t1, true);
+ }
+
+ public P3T1 getT1() {
+ return (P3T1)readProperty("t1");
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "t1":
+ return this.t1;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "t1":
+ this.t1 = val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.t1);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.t1 = in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T3.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T3.java
new file mode 100644
index 0000000..00d002e
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T3.java
@@ -0,0 +1,91 @@
+package io.bootique.cayenne.v50.junit5.persistence3.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+
+import io.bootique.cayenne.v50.junit5.persistence3.P3T1;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T3;
+
+/**
+ * Class _P3T3 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _P3T3 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(P3T3.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final EntityProperty T1 = PropertyFactory.createEntity("t1", P3T1.class);
+
+
+ protected Object t1;
+
+ public void setT1(P3T1 t1) {
+ setToOneTarget("t1", t1, true);
+ }
+
+ public P3T1 getT1() {
+ return (P3T1)readProperty("t1");
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "t1":
+ return this.t1;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "t1":
+ this.t1 = val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.t1);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.t1 = in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T4.java b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T4.java
new file mode 100644
index 0000000..42dbff2
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/java/io/bootique/cayenne/v50/junit5/persistence3/auto/_P3T4.java
@@ -0,0 +1,97 @@
+package io.bootique.cayenne.v50.junit5.persistence3.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+
+import io.bootique.cayenne.v50.junit5.persistence3.P3T1;
+import io.bootique.cayenne.v50.junit5.persistence3.P3T4;
+
+/**
+ * Class _P3T4 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _P3T4 extends PersistentObject {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final SelfProperty SELF = PropertyFactory.createSelf(P3T4.class);
+
+ public static final String ID_PK_COLUMN = "id";
+
+ public static final ListProperty T1S = PropertyFactory.createList("t1s", P3T1.class);
+
+
+ protected Object t1s;
+
+ public void addToT1s(P3T1 obj) {
+ addToManyTarget("t1s", obj, true);
+ }
+
+ public void removeFromT1s(P3T1 obj) {
+ removeToManyTarget("t1s", obj, true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public List getT1s() {
+ return (List)readProperty("t1s");
+ }
+
+ @Override
+ public Object readPropertyDirectly(String propName) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch(propName) {
+ case "t1s":
+ return this.t1s;
+ default:
+ return super.readPropertyDirectly(propName);
+ }
+ }
+
+ @Override
+ public void writePropertyDirectly(String propName, Object val) {
+ if(propName == null) {
+ throw new IllegalArgumentException();
+ }
+
+ switch (propName) {
+ case "t1s":
+ this.t1s = val;
+ break;
+ default:
+ super.writePropertyDirectly(propName, val);
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ writeSerialized(out);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ readSerialized(in);
+ }
+
+ @Override
+ protected void writeState(ObjectOutputStream out) throws IOException {
+ super.writeState(out);
+ out.writeObject(this.t1s);
+ }
+
+ @Override
+ protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ super.readState(in);
+ this.t1s = in.readObject();
+ }
+
+}
diff --git a/bootique-cayenne50-junit5/src/test/resources/cayenne-project2.xml b/bootique-cayenne50-junit5/src/test/resources/cayenne-project2.xml
new file mode 100644
index 0000000..e133347
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/cayenne-project2.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/bootique-cayenne50-junit5/src/test/resources/cayenne-project3.xml b/bootique-cayenne50-junit5/src/test/resources/cayenne-project3.xml
new file mode 100644
index 0000000..f85c93e
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/cayenne-project3.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/bootique-cayenne50-junit5/src/test/resources/config-noautocommit.yml b/bootique-cayenne50-junit5/src/test/resources/config-noautocommit.yml
new file mode 100644
index 0000000..3aae5ce
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/config-noautocommit.yml
@@ -0,0 +1,24 @@
+# Licensed to ObjectStyle LLC under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ObjectStyle LLC 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.
+
+jdbc:
+ ds:
+ jdbcUrl: jdbc:derby:target/derby/derby3;create=true
+ autoCommit: false
+
+cayenne:
+ datasource: ds
+ configs:
+ - cayenne-project2.xml
diff --git a/bootique-cayenne50-junit5/src/test/resources/config2.yml b/bootique-cayenne50-junit5/src/test/resources/config2.yml
new file mode 100644
index 0000000..774d2bf
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/config2.yml
@@ -0,0 +1,18 @@
+# Licensed to ObjectStyle LLC under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ObjectStyle LLC 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.
+
+cayenne:
+ configs:
+ - cayenne-project2.xml
diff --git a/bootique-cayenne50-junit5/src/test/resources/config3.yml b/bootique-cayenne50-junit5/src/test/resources/config3.yml
new file mode 100644
index 0000000..bd189dd
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/config3.yml
@@ -0,0 +1,18 @@
+# Licensed to ObjectStyle LLC under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ObjectStyle LLC 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.
+
+cayenne:
+ configs:
+ - cayenne-project3.xml
diff --git a/bootique-cayenne50-junit5/src/test/resources/datamap2.map.xml b/bootique-cayenne50-junit5/src/test/resources/datamap2.map.xml
new file mode 100644
index 0000000..9648b46
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/datamap2.map.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Default
+ ../java
+ entity
+ templates/v4_1/subclass.vm
+ templates/v4_1/superclass.vm
+ templates/v4_1/embeddable-subclass.vm
+ templates/v4_1/embeddable-superclass.vm
+ templates/v4_1/datamap-subclass.vm
+ templates/v4_1/datamap-superclass.vm
+ *.java
+ true
+ true
+ false
+ false
+ false
+
+
diff --git a/bootique-cayenne50-junit5/src/test/resources/datamap3.map.xml b/bootique-cayenne50-junit5/src/test/resources/datamap3.map.xml
new file mode 100644
index 0000000..0bba4f8
--- /dev/null
+++ b/bootique-cayenne50-junit5/src/test/resources/datamap3.map.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Default
+ ../java
+ entity
+ templates/v4_1/subclass.vm
+ templates/v4_1/superclass.vm
+ templates/v4_1/embeddable-subclass.vm
+ templates/v4_1/embeddable-superclass.vm
+ templates/v4_1/datamap-subclass.vm
+ templates/v4_1/datamap-superclass.vm
+ *.java
+ true
+ true
+ false
+ false
+ false
+
+
diff --git a/bootique-cayenne50/pom.xml b/bootique-cayenne50/pom.xml
new file mode 100644
index 0000000..a3ef4cc
--- /dev/null
+++ b/bootique-cayenne50/pom.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+ 4.0.0
+
+ io.bootique.cayenne
+ bootique-cayenne-parent
+ 3.0-SNAPSHOT
+
+
+ bootique-cayenne50
+ jar
+
+ bootique-cayenne50: Cayenne Integration Bundle for Bootique
+ Provides Apache Cayenne integration with Bootique
+
+
+
+
+ org.apache.cayenne
+ cayenne
+ ${cayenne50.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.apache.cayenne
+ cayenne-commitlog
+ ${cayenne50.version}
+
+
+
+
+
+
+
+
+ io.bootique
+ bootique
+
+
+ io.bootique.jdbc
+ bootique-jdbc
+
+
+ org.apache.cayenne
+ cayenne
+
+
+ org.apache.cayenne
+ cayenne-commitlog
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+ io.bootique.jdbc
+ bootique-jdbc-junit5-derby
+ test
+
+
+ io.bootique.jdbc
+ bootique-jdbc-hikaricp
+ test
+
+
+
+
+
+
+ gpg
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+ rat
+
+
+
+ org.apache.rat
+ apache-rat-plugin
+
+
+ derby.log
+
+
+
+
+
+
+
+
diff --git a/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/BQCayenneDataSourceFactory.java b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/BQCayenneDataSourceFactory.java
new file mode 100644
index 0000000..0bb9dee
--- /dev/null
+++ b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/BQCayenneDataSourceFactory.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50;
+
+import io.bootique.jdbc.DataSourceFactory;
+import org.apache.cayenne.configuration.DataNodeDescriptor;
+import org.apache.cayenne.configuration.runtime.DelegatingDataSourceFactory;
+
+import javax.sql.DataSource;
+import java.util.Collection;
+
+public class BQCayenneDataSourceFactory extends DelegatingDataSourceFactory {
+
+ private static final String PARAM_PREFIX = "bqds:";
+
+ private DataSourceFactory bqDataSourceFactory;
+ private String defaultDataSourceName;
+
+ public BQCayenneDataSourceFactory(DataSourceFactory bqDataSourceFactory, String defaultDataSourceName) {
+ this.bqDataSourceFactory = bqDataSourceFactory;
+ this.defaultDataSourceName = defaultDataSourceName;
+ }
+
+ static String encodeDataSourceRef(String bqDataSource) {
+ return PARAM_PREFIX + bqDataSource;
+ }
+
+ static String decodeDataSourceRef(String ref, String defaultName) {
+ return ref != null && ref.startsWith(PARAM_PREFIX) ? ref.substring(PARAM_PREFIX.length()) : defaultName;
+ }
+
+ @Override
+ public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) throws Exception {
+
+ DataSource dataSource;
+
+ // 1. try DataSource explicitly mapped in Bootique
+ dataSource = mappedBootiqueDataSource(nodeDescriptor);
+ if (dataSource != null) {
+ return dataSource;
+ }
+
+ // 2. try loading from Cayenne XML
+ dataSource = cayenneDataSource(nodeDescriptor);
+ if (dataSource != null) {
+ return dataSource;
+ }
+
+ // 3. try default DataSource from Bootique
+ dataSource = defaultBootiqueDataSource();
+ if (dataSource != null) {
+ return dataSource;
+ }
+
+ // 4. throw
+ return throwOnNoDataSource();
+ }
+
+ protected DataSource throwOnNoDataSource() {
+ Collection names = bqDataSourceFactory.allNames();
+ if (names.isEmpty()) {
+ throw new IllegalStateException("No DataSources are available for Cayenne. " +
+ "Add a DataSource via 'bootique-jdbc' or map it in Cayenne project.");
+ }
+
+ if (defaultDataSourceName == null) {
+ throw new IllegalStateException(
+ String.format("Can't map Cayenne DataSource: 'cayenne.datasource' is missing. " +
+ "Available DataSources are %s", names));
+ }
+
+ throw new IllegalStateException(
+ String.format("Can't map Cayenne DataSource: 'cayenne.datasource' is set to '%s'. " +
+ "Available DataSources: %s", defaultDataSourceName, names));
+ }
+
+ protected DataSource cayenneDataSource(DataNodeDescriptor nodeDescriptor) throws Exception {
+
+ // trying to guess whether Cayenne will be able to provide a DataSource without our help...
+ if (shouldConfigureDataSourceFromProperties(nodeDescriptor)
+ || nodeDescriptor.getDataSourceFactoryType() != null
+ || nodeDescriptor.getDataSourceDescriptor() != null) {
+
+ return super.getDataSource(nodeDescriptor);
+ }
+
+ return null;
+ }
+
+ protected DataSource defaultBootiqueDataSource() {
+ Collection names = bqDataSourceFactory.allNames();
+ if (names.size() == 1) {
+ return mappedBootiqueDataSource(names.iterator().next());
+ }
+
+ return null;
+ }
+
+ protected DataSource mappedBootiqueDataSource(DataNodeDescriptor nodeDescriptor) {
+ String datasource = decodeDataSourceRef(nodeDescriptor.getParameters(), defaultDataSourceName);
+ return mappedBootiqueDataSource(datasource);
+ }
+
+ protected DataSource mappedBootiqueDataSource(String datasource) {
+
+ if (datasource == null) {
+ return null;
+ }
+
+ DataSource ds = bqDataSourceFactory.forName(datasource);
+ if (ds == null) {
+ throw new IllegalStateException("Unknown 'defaultDataSourceName': " + datasource);
+ }
+
+ return ds;
+ }
+}
diff --git a/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneConfigMerger.java b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneConfigMerger.java
new file mode 100644
index 0000000..ecbab83
--- /dev/null
+++ b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneConfigMerger.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * A simple merger that uses "last wins" strategy, returning the last collection passed to the method.
+ */
+public class CayenneConfigMerger {
+
+ public Collection merge(Collection configs1, Collection configs2) {
+ return configs2 == null || configs2.isEmpty() ? Objects.requireNonNull(configs1) : configs2;
+ }
+}
diff --git a/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneModule.java b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneModule.java
new file mode 100644
index 0000000..715e0da
--- /dev/null
+++ b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneModule.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50;
+
+import io.bootique.BQModule;
+import io.bootique.ModuleCrate;
+import io.bootique.config.ConfigurationFactory;
+import io.bootique.di.Binder;
+import io.bootique.di.Provides;
+import org.apache.cayenne.runtime.CayenneRuntime;
+
+import javax.inject.Singleton;
+
+/**
+ * @since 2.0
+ */
+public class CayenneModule implements BQModule {
+
+ private static final String CONFIG_PREFIX = "cayenne";
+
+ /**
+ * @param binder DI binder passed to the Module that invokes this method.
+ * @return an instance of {@link CayenneModuleExtender} that can be used to load Cayenne custom extensions.
+ */
+ public static CayenneModuleExtender extend(Binder binder) {
+ return new CayenneModuleExtender(binder);
+ }
+
+ @Override
+ public ModuleCrate crate() {
+ return ModuleCrate.of(this)
+ .description("Integrates Apache Cayenne ORM, v4.2")
+ .config(CONFIG_PREFIX, CayenneRuntimeFactory.class)
+ .build();
+ }
+
+ @Override
+ public void configure(Binder binder) {
+ extend(binder).initAllExtensions();
+ }
+
+ @Provides
+ @Singleton
+ CayenneConfigMerger provideConfigMerger() {
+ return new CayenneConfigMerger();
+ }
+
+ @Provides
+ @Singleton
+ CayenneRuntime createCayenneRuntime(ConfigurationFactory configFactory) {
+ return configFactory.config(CayenneRuntimeFactory.class, CONFIG_PREFIX).create();
+ }
+}
diff --git a/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneModuleExtender.java b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneModuleExtender.java
new file mode 100644
index 0000000..fb299cb
--- /dev/null
+++ b/bootique-cayenne50/src/main/java/io/bootique/cayenne/v50/CayenneModuleExtender.java
@@ -0,0 +1,291 @@
+/*
+ * Licensed to ObjectStyle LLC under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ObjectStyle LLC 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 io.bootique.cayenne.v50;
+
+import io.bootique.ModuleExtender;
+import io.bootique.cayenne.v50.annotation.CayenneConfigs;
+import io.bootique.cayenne.v50.annotation.CayenneListener;
+import io.bootique.cayenne.v50.commitlog.MappedCommitLogListener;
+import io.bootique.cayenne.v50.commitlog.MappedCommitLogListenerType;
+import io.bootique.cayenne.v50.syncfilter.MappedDataChannelSyncFilter;
+import io.bootique.cayenne.v50.syncfilter.MappedDataChannelSyncFilterType;
+import io.bootique.di.Binder;
+import io.bootique.di.Key;
+import io.bootique.di.SetBuilder;
+import org.apache.cayenne.DataChannelQueryFilter;
+import org.apache.cayenne.DataChannelSyncFilter;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.access.types.ValueObjectType;
+import org.apache.cayenne.commitlog.CommitLogListener;
+import org.apache.cayenne.di.Module;
+
+public class CayenneModuleExtender extends ModuleExtender {
+
+ static final String COMMIT_LOG_ANNOTATION = CayenneModuleExtender.class.getPackageName() + ".commit_log_annotation";
+
+ private SetBuilder syncFilters;
+ private SetBuilder syncFilterTypes;
+ private SetBuilder queryFilters;
+ private SetBuilder