IGNITE-15674 Migrate CacheSpringStoreSessionListener to ignite-extensions. (#74)
diff --git a/modules/spring-boot-autoconfigure-ext/pom.xml b/modules/spring-boot-autoconfigure-ext/pom.xml
index 9b7657f..d5ce646 100644
--- a/modules/spring-boot-autoconfigure-ext/pom.xml
+++ b/modules/spring-boot-autoconfigure-ext/pom.xml
@@ -71,6 +71,13 @@
</dependency>
<dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-tx</artifactId>
+ <version>${spring-5.2.version}</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-log4j</artifactId>
<version>${ignite.version}</version>
diff --git a/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml b/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml
index 500705c..167230a 100644
--- a/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml
+++ b/modules/spring-boot-thin-client-autoconfigure-ext/pom.xml
@@ -72,6 +72,13 @@
</dependency>
<dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-tx</artifactId>
+ <version>${spring-5.2.version}</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-log4j</artifactId>
<version>${ignite.version}</version>
diff --git a/modules/spring-data-2.0-ext/pom.xml b/modules/spring-data-2.0-ext/pom.xml
index 3bc8b92..9fcfda7 100644
--- a/modules/spring-data-2.0-ext/pom.xml
+++ b/modules/spring-data-2.0-ext/pom.xml
@@ -79,48 +79,11 @@
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
- <artifactId>spring-core</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-beans</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-aop</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-expression</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-tx</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-jdbc</artifactId>
- </exclusion>
</exclusions>
</dependency>
- <!--Remove spring-core and spring-beans dependencies while upgrading ignite-spring version to 5.0-->
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-core</artifactId>
- <version>${spring-5.0.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-beans</artifactId>
- <version>${spring-5.0.version}</version>
- </dependency>
-
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
@@ -128,12 +91,6 @@
</dependency>
<dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-tx</artifactId>
- <version>${spring-5.0.version}</version>
- </dependency>
-
- <dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons.lang.version}</version>
diff --git a/modules/spring-data-2.2-ext/pom.xml b/modules/spring-data-2.2-ext/pom.xml
index a30eb6e..3f2ee20 100644
--- a/modules/spring-data-2.2-ext/pom.xml
+++ b/modules/spring-data-2.2-ext/pom.xml
@@ -79,61 +79,19 @@
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
- <artifactId>spring-core</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-beans</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-aop</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-expression</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-tx</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-jdbc</artifactId>
- </exclusion>
</exclusions>
</dependency>
<!--Remove spring-core and spring-beans dependencies while upgrading ignite-spring version to 5.2-->
<dependency>
<groupId>org.springframework</groupId>
- <artifactId>spring-core</artifactId>
- <version>${spring-5.2.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-beans</artifactId>
- <version>${spring-5.2.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-5.2.version}</version>
</dependency>
<dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-tx</artifactId>
- <version>${spring-5.2.version}</version>
- </dependency>
-
- <dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons.lang.version}</version>
diff --git a/modules/spring-tx-ext/examples/config/example-default.xml b/modules/spring-tx-ext/examples/config/example-default.xml
new file mode 100644
index 0000000..e6c359d
--- /dev/null
+++ b/modules/spring-tx-ext/examples/config/example-default.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+ Ignite configuration with all defaults and enabled p2p deployment and enabled events.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xsi:schemaLocation="
+ http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/util
+ http://www.springframework.org/schema/util/spring-util.xsd">
+ <bean abstract="true" id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+ <!-- Set to true to enable distributed class loading for examples, default is false. -->
+ <property name="peerClassLoadingEnabled" value="true"/>
+
+ <!-- Enable task execution events for examples. -->
+ <property name="includeEventTypes">
+ <list>
+ <!--Task execution events-->
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_STARTED"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FINISHED"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FAILED"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_TIMEDOUT"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_SESSION_ATTR_SET"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_REDUCED"/>
+
+ <!--Cache events-->
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_PUT"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_READ"/>
+ <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_REMOVED"/>
+ </list>
+ </property>
+
+ <!-- Explicitly configure TCP discovery SPI to provide list of initial nodes. -->
+ <property name="discoverySpi">
+ <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+ <property name="ipFinder">
+ <!--
+ Ignite provides several options for automatic discovery that can be used
+ instead os static IP based discovery. For information on all options refer
+ to our documentation: http://apacheignite.readme.io/docs/cluster-config
+ -->
+ <!-- Uncomment static IP finder to enable static-based discovery of initial nodes. -->
+ <!--<bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">-->
+ <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
+ <property name="addresses">
+ <list>
+ <!-- In distributed environment, replace with actual host IP address. -->
+ <value>127.0.0.1:47500..47509</value>
+ </list>
+ </property>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ </bean>
+</beans>
diff --git a/modules/spring-tx-ext/examples/config/example-ignite.xml b/modules/spring-tx-ext/examples/config/example-ignite.xml
new file mode 100644
index 0000000..a3e7e22
--- /dev/null
+++ b/modules/spring-tx-ext/examples/config/example-ignite.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+ Ignite configuration with all defaults and enabled p2p deployment and enabled events.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans.xsd">
+ <!-- Imports default Ignite configuration -->
+ <import resource="example-default.xml"/>
+
+ <bean parent="ignite.cfg"/>
+</beans>
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExampleNodeStartup.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExampleNodeStartup.java
new file mode 100644
index 0000000..88c4c9c
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExampleNodeStartup.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples;
+
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+
+/**
+ * Starts up an empty node with example compute configuration.
+ */
+public class ExampleNodeStartup {
+ /**
+ * Start up an empty node with example compute configuration.
+ *
+ * @param args Command line arguments, none required.
+ * @throws IgniteException If failed.
+ */
+ public static void main(String[] args) throws IgniteException {
+ Ignition.start("modules/spring-tx-ext/examples/config/example-ignite.xml");
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExamplesUtils.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExamplesUtils.java
new file mode 100644
index 0000000..7b0a1cd
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/ExamplesUtils.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples;
+
+import java.net.URL;
+import java.util.List;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.cluster.ClusterGroup;
+
+/**
+ *
+ */
+public class ExamplesUtils {
+ /** */
+ private static final ClassLoader CLS_LDR = ExamplesUtils.class.getClassLoader();
+
+ /**
+ * Exits with code {@code -1} if maximum memory is below 90% of minimally allowed threshold.
+ *
+ * @param min Minimum memory threshold.
+ */
+ public static void checkMinMemory(long min) {
+ long maxMem = Runtime.getRuntime().maxMemory();
+
+ if (maxMem < .85 * min) {
+ System.err.println("Heap limit is too low (" + (maxMem / (1024 * 1024)) +
+ "MB), please increase heap size at least up to " + (min / (1024 * 1024)) + "MB.");
+
+ System.exit(-1);
+ }
+ }
+
+ /**
+ * Returns URL resolved by class loader for classes in examples project.
+ *
+ * @return Resolved URL.
+ */
+ public static URL url(String path) {
+ URL url = CLS_LDR.getResource(path);
+
+ if (url == null)
+ throw new RuntimeException("Failed to resolve resource URL by path: " + path);
+
+ return url;
+ }
+
+ /**
+ * Checks minimum topology size for running a certain example.
+ *
+ * @param grp Cluster to check size for.
+ * @param size Minimum number of nodes required to run a certain example.
+ * @return {@code True} if check passed, {@code false} otherwise.
+ */
+ public static boolean checkMinTopologySize(ClusterGroup grp, int size) {
+ int prjSize = grp.nodes().size();
+
+ if (prjSize < size) {
+ System.err.println(">>> Please start at least " + size + " cluster nodes to run example.");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if cluster has server nodes.
+ *
+ * @param ignite Ignite instance.
+ * @return {@code True} if cluster has server nodes, {@code false} otherwise.
+ */
+ public static boolean hasServerNodes(Ignite ignite) {
+ if (ignite.cluster().forServers().nodes().isEmpty()) {
+ System.err.println("Server nodes not found (start data nodes with ExampleNodeStartup class)");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Convenience method for printing query results.
+ *
+ * @param res Query results.
+ */
+ public static void printQueryResults(List<?> res) {
+ if (res == null || res.isEmpty())
+ System.out.println("Query result set is empty.");
+ else {
+ for (Object row : res) {
+ if (row instanceof List) {
+ System.out.print("(");
+
+ List<?> l = (List)row;
+
+ for (int i = 0; i < l.size(); i++) {
+ Object o = l.get(i);
+
+ if (o instanceof Double || o instanceof Float)
+ System.out.printf("%.2f", o);
+ else
+ System.out.print(l.get(i));
+
+ if (i + 1 != l.size())
+ System.out.print(',');
+ }
+
+ System.out.println(')');
+ }
+ else
+ System.out.println(" " + row);
+ }
+ }
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringPersonStore.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringPersonStore.java
new file mode 100644
index 0000000..c4b52bb
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringPersonStore.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.datagrid.store.spring;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.cache.Cache;
+import javax.cache.integration.CacheLoaderException;
+import javax.sql.DataSource;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.cache.store.CacheStore;
+import org.apache.ignite.cache.store.CacheStoreAdapter;
+import org.apache.ignite.examples.model.Person;
+import org.apache.ignite.lang.IgniteBiInClosure;
+import org.h2.jdbcx.JdbcConnectionPool;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowCallbackHandler;
+import org.springframework.jdbc.core.RowMapper;
+
+/**
+ * Example of {@link CacheStore} implementation that uses JDBC
+ * transaction with cache transactions and maps {@link Long} to {@link Person}.
+ */
+public class CacheSpringPersonStore extends CacheStoreAdapter<Long, Person> {
+ /** Data source. */
+ public static final DataSource DATA_SRC =
+ JdbcConnectionPool.create("jdbc:h2:tcp://localhost/mem:ExampleDb", "sa", "");
+
+ /** Spring JDBC template. */
+ private JdbcTemplate jdbcTemplate;
+
+ /**
+ * Constructor.
+ *
+ * @throws IgniteException If failed.
+ */
+ public CacheSpringPersonStore() throws IgniteException {
+ jdbcTemplate = new JdbcTemplate(DATA_SRC);
+ }
+
+ /** {@inheritDoc} */
+ @Override public Person load(Long key) {
+ System.out.println(">>> Store load [key=" + key + ']');
+
+ try {
+ return jdbcTemplate.queryForObject("select * from PERSON where id = ?", new RowMapper<Person>() {
+ @Override public Person mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return new Person(rs.getLong(1), rs.getString(2), rs.getString(3));
+ }
+ }, key);
+ }
+ catch (EmptyResultDataAccessException ignored) {
+ return null;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public void write(Cache.Entry<? extends Long, ? extends Person> entry) {
+ Long key = entry.getKey();
+ Person val = entry.getValue();
+
+ System.out.println(">>> Store write [key=" + key + ", val=" + val + ']');
+
+ int updated = jdbcTemplate.update("update PERSON set first_name = ?, last_name = ? where id = ?",
+ val.firstName, val.lastName, val.id);
+
+ if (updated == 0) {
+ jdbcTemplate.update("insert into PERSON (id, first_name, last_name) values (?, ?, ?)",
+ val.id, val.firstName, val.lastName);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(Object key) {
+ System.out.println(">>> Store delete [key=" + key + ']');
+
+ jdbcTemplate.update("delete from PERSON where id = ?", key);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void loadCache(final IgniteBiInClosure<Long, Person> clo, Object... args) {
+ if (args == null || args.length == 0 || args[0] == null)
+ throw new CacheLoaderException("Expected entry count parameter is not provided.");
+
+ int entryCnt = (Integer)args[0];
+
+ final AtomicInteger cnt = new AtomicInteger();
+
+ jdbcTemplate.query("select * from PERSON limit ?", new RowCallbackHandler() {
+ @Override public void processRow(ResultSet rs) throws SQLException {
+ Person person = new Person(rs.getLong(1), rs.getString(2), rs.getString(3));
+
+ clo.apply(person.id, person);
+
+ cnt.incrementAndGet();
+ }
+ }, entryCnt);
+
+ System.out.println(">>> Loaded " + cnt + " values into cache.");
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringStoreExample.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringStoreExample.java
new file mode 100644
index 0000000..22bf7ae
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/CacheSpringStoreExample.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.datagrid.store.spring;
+
+import java.util.UUID;
+import javax.cache.configuration.Factory;
+import javax.cache.configuration.FactoryBuilder;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.cache.store.CacheStoreSessionListener;
+import org.apache.ignite.cache.store.spring.CacheSpringStoreSessionListener;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.examples.ExampleNodeStartup;
+import org.apache.ignite.examples.ExamplesUtils;
+import org.apache.ignite.examples.model.Person;
+import org.apache.ignite.examples.util.DbH2ServerStartup;
+import org.apache.ignite.transactions.Transaction;
+
+import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
+
+/**
+ * Demonstrates usage of cache with underlying persistent store configured.
+ * <p>
+ * This example uses {@link CacheSpringPersonStore} as a persistent store.
+ * <p>
+ * To start the example, you should:
+ * <ul>
+ * <li>Start H2 database TCP server using {@link DbH2ServerStartup}.</li>
+ * <li>Start a few nodes using {@link ExampleNodeStartup}.</li>
+ * <li>Start example using {@link CacheSpringStoreExample}.</li>
+ * </ul>
+ * <p>
+ * Remote nodes can be started with {@link ExampleNodeStartup} in another JVM which will
+ * start node with {@code examples/config/example-ignite.xml} configuration.
+ */
+public class CacheSpringStoreExample {
+ /** Cache name. */
+ private static final String CACHE_NAME = CacheSpringStoreExample.class.getSimpleName();
+
+ /** Heap size required to run this example. */
+ public static final int MIN_MEMORY = 1024 * 1024 * 1024;
+
+ /** Number of entries to load. */
+ private static final int ENTRY_COUNT = 100_000;
+
+ /** Global person ID to use across entire example. */
+ private static final Long id = Math.abs(UUID.randomUUID().getLeastSignificantBits());
+
+ /**
+ * Executes example.
+ *
+ * @param args Command line arguments, none required.
+ * @throws IgniteException If example execution failed.
+ */
+ public static void main(String[] args) throws IgniteException {
+ ExamplesUtils.checkMinMemory(MIN_MEMORY);
+
+ // To start ignite with desired configuration uncomment the appropriate line.
+ try (Ignite ignite = Ignition.start("modules/spring-tx-ext/examples/config/example-ignite.xml")) {
+ System.out.println();
+ System.out.println(">>> Cache store example started.");
+
+ CacheConfiguration<Long, Person> cacheCfg = new CacheConfiguration<>(CACHE_NAME);
+
+ // Set atomicity as transaction, since we are showing transactions in example.
+ cacheCfg.setAtomicityMode(TRANSACTIONAL);
+
+ // Configure Spring store.
+ cacheCfg.setCacheStoreFactory(FactoryBuilder.factoryOf(CacheSpringPersonStore.class));
+
+ // Configure Spring session listener.
+ cacheCfg.setCacheStoreSessionListenerFactories(new Factory<CacheStoreSessionListener>() {
+ @Override public CacheStoreSessionListener create() {
+ CacheSpringStoreSessionListener lsnr = new CacheSpringStoreSessionListener();
+
+ lsnr.setDataSource(CacheSpringPersonStore.DATA_SRC);
+
+ return lsnr;
+ }
+ });
+
+ cacheCfg.setReadThrough(true);
+ cacheCfg.setWriteThrough(true);
+
+ // Auto-close cache at the end of the example.
+ try (IgniteCache<Long, Person> cache = ignite.getOrCreateCache(cacheCfg)) {
+ // Make initial cache loading from persistent store. This is a
+ // distributed operation and will call CacheStore.loadCache(...)
+ // method on all nodes in topology.
+ loadCache(cache);
+
+ // Start transaction and execute several cache operations with
+ // read/write-through to persistent store.
+ executeTransaction(cache);
+ }
+ finally {
+ // Distributed cache could be removed from cluster only by #destroyCache() call.
+ ignite.destroyCache(CACHE_NAME);
+ }
+ }
+ }
+
+ /**
+ * Makes initial cache loading.
+ *
+ * @param cache Cache to load.
+ */
+ private static void loadCache(IgniteCache<Long, Person> cache) {
+ long start = System.currentTimeMillis();
+
+ // Start loading cache from persistent store on all caching nodes.
+ cache.loadCache(null, ENTRY_COUNT);
+
+ long end = System.currentTimeMillis();
+
+ System.out.println(">>> Loaded " + cache.size() + " keys with backups in " + (end - start) + "ms.");
+ }
+
+ /**
+ * Executes transaction with read/write-through to persistent store.
+ *
+ * @param cache Cache to execute transaction on.
+ */
+ private static void executeTransaction(IgniteCache<Long, Person> cache) {
+ try (Transaction tx = Ignition.ignite().transactions().txStart()) {
+ Person val = cache.get(id);
+
+ System.out.println("Read value: " + val);
+
+ val = cache.getAndPut(id, new Person(id, "Isaac", "Newton"));
+
+ System.out.println("Overwrote old value: " + val);
+
+ val = cache.get(id);
+
+ System.out.println("Read value: " + val);
+
+ tx.commit();
+ }
+
+ System.out.println("Read value after commit: " + cache.get(id));
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/package-info.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/package-info.java
new file mode 100644
index 0000000..211239f
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/datagrid/store/spring/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * <!-- Package description. -->
+ * Contains Spring-based cache store implementation.
+ */
+package org.apache.ignite.examples.datagrid.store.spring;
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Address.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Address.java
new file mode 100644
index 0000000..159a5ef
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Address.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.model;
+
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.binary.BinaryReader;
+import org.apache.ignite.binary.BinaryWriter;
+import org.apache.ignite.binary.Binarylizable;
+
+/**
+ * Employee address.
+ * <p>
+ * This class implements {@link org.apache.ignite.binary.Binarylizable} only for example purposes,
+ * in order to show how to customize serialization and deserialization of
+ * binary objects.
+ */
+public class Address implements Binarylizable {
+ /** Street. */
+ private String street;
+
+ /** ZIP code. */
+ private int zip;
+
+ /**
+ * Required for binary deserialization.
+ */
+ public Address() {
+ // No-op.
+ }
+
+ /**
+ * @param street Street.
+ * @param zip ZIP code.
+ */
+ public Address(String street, int zip) {
+ this.street = street;
+ this.zip = zip;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void writeBinary(BinaryWriter writer) throws BinaryObjectException {
+ writer.writeString("street", street);
+ writer.writeInt("zip", zip);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void readBinary(BinaryReader reader) throws BinaryObjectException {
+ street = reader.readString("street");
+ zip = reader.readInt("zip");
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "Address [street=" + street +
+ ", zip=" + zip + ']';
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Employee.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Employee.java
new file mode 100644
index 0000000..a59ffce
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Employee.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.model;
+
+import java.util.Collection;
+
+/**
+ * This class represents employee object.
+ */
+public class Employee {
+ /** Name. */
+ private String name;
+
+ /** Salary. */
+ private long salary;
+
+ /** Address. */
+ private Address addr;
+
+ /** Departments. */
+ private Collection<String> departments;
+
+ /**
+ * Required for binary deserialization.
+ */
+ public Employee() {
+ // No-op.
+ }
+
+ /**
+ * @param name Name.
+ * @param salary Salary.
+ * @param addr Address.
+ * @param departments Departments.
+ */
+ public Employee(String name, long salary, Address addr, Collection<String> departments) {
+ this.name = name;
+ this.salary = salary;
+ this.addr = addr;
+ this.departments = departments;
+ }
+
+ /**
+ * @return Name.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * @return Salary.
+ */
+ public long salary() {
+ return salary;
+ }
+
+ /**
+ * @return Address.
+ */
+ public Address address() {
+ return addr;
+ }
+
+ /**
+ * @return Departments.
+ */
+ public Collection<String> departments() {
+ return departments;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "Employee [name=" + name +
+ ", salary=" + salary +
+ ", address=" + addr +
+ ", departments=" + departments + ']';
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/EmployeeKey.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/EmployeeKey.java
new file mode 100644
index 0000000..55e7967
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/EmployeeKey.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.model;
+
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/**
+ * This class represents key for employee object.
+ * <p>
+ * Used in query example to collocate employees
+ * with their organizations.
+ */
+public class EmployeeKey {
+ /** ID. */
+ private int id;
+
+ /** Organization ID. */
+ @AffinityKeyMapped
+ private int organizationId;
+
+ /**
+ * Required for binary deserialization.
+ */
+ public EmployeeKey() {
+ // No-op.
+ }
+
+ /**
+ * @param id ID.
+ * @param organizationId Organization ID.
+ */
+ public EmployeeKey(int id, int organizationId) {
+ this.id = id;
+ this.organizationId = organizationId;
+ }
+
+ /**
+ * @return ID.
+ */
+ public int id() {
+ return id;
+ }
+
+ /**
+ * @return Organization ID.
+ */
+ public int organizationId() {
+ return organizationId;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ EmployeeKey key = (EmployeeKey)o;
+
+ return id == key.id && organizationId == key.organizationId;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ int res = id;
+
+ res = 31 * res + organizationId;
+
+ return res;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "EmployeeKey [id=" + id +
+ ", organizationId=" + organizationId + ']';
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Organization.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Organization.java
new file mode 100644
index 0000000..fc90c07
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Organization.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.model;
+
+import java.sql.Timestamp;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+
+/**
+ * This class represents organization object.
+ */
+public class Organization {
+ /** */
+ private static final AtomicLong ID_GEN = new AtomicLong();
+
+ /** Organization ID (indexed). */
+ @QuerySqlField(index = true)
+ private Long id;
+
+ /** Organization name (indexed). */
+ @QuerySqlField(index = true)
+ private String name;
+
+ /** Address. */
+ private Address addr;
+
+ /** Type. */
+ private OrganizationType type;
+
+ /** Last update time. */
+ private Timestamp lastUpdated;
+
+ /**
+ * Required for binary deserialization.
+ */
+ public Organization() {
+ // No-op.
+ }
+
+ /**
+ * @param name Organization name.
+ */
+ public Organization(String name) {
+ id = ID_GEN.incrementAndGet();
+
+ this.name = name;
+ }
+
+ /**
+ * @param id Organization ID.
+ * @param name Organization name.
+ */
+ public Organization(long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ /**
+ * @param name Name.
+ * @param addr Address.
+ * @param type Type.
+ * @param lastUpdated Last update time.
+ */
+ public Organization(String name, Address addr, OrganizationType type, Timestamp lastUpdated) {
+ id = ID_GEN.incrementAndGet();
+
+ this.name = name;
+ this.addr = addr;
+ this.type = type;
+
+ this.lastUpdated = lastUpdated;
+ }
+
+ /**
+ * @return Organization ID.
+ */
+ public Long id() {
+ return id;
+ }
+
+ /**
+ * @return Name.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * @return Address.
+ */
+ public Address address() {
+ return addr;
+ }
+
+ /**
+ * @return Type.
+ */
+ public OrganizationType type() {
+ return type;
+ }
+
+ /**
+ * @return Last update time.
+ */
+ public Timestamp lastUpdated() {
+ return lastUpdated;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "Organization [id=" + id +
+ ", name=" + name +
+ ", address=" + addr +
+ ", type=" + type +
+ ", lastUpdated=" + lastUpdated + ']';
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/OrganizationType.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/OrganizationType.java
new file mode 100644
index 0000000..8b22600
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/OrganizationType.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.model;
+
+/**
+ * Organization type enum.
+ */
+public enum OrganizationType {
+ /** Non-profit organization. */
+ NON_PROFIT,
+
+ /** Private organization. */
+ PRIVATE,
+
+ /** Government organization. */
+ GOVERNMENT
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Person.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Person.java
new file mode 100644
index 0000000..6d3a6df
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/Person.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.model;
+
+import java.io.Serializable;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.cache.affinity.AffinityKey;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * Person class.
+ */
+public class Person implements Serializable {
+ /** */
+ private static final AtomicLong ID_GEN = new AtomicLong();
+
+ /** Person ID (indexed). */
+ @QuerySqlField(index = true)
+ public Long id;
+
+ /** Organization ID (indexed). */
+ @QuerySqlField(index = true)
+ public Long orgId;
+
+ /** First name (not-indexed). */
+ @QuerySqlField
+ public String firstName;
+
+ /** Last name (not indexed). */
+ @QuerySqlField
+ public String lastName;
+
+ /** Resume text (create LUCENE-based TEXT index for this field). */
+ @QueryTextField
+ public String resume;
+
+ /** Salary (indexed). */
+ @QuerySqlField(index = true)
+ public double salary;
+
+ /** Custom cache key to guarantee that person is always collocated with its organization. */
+ private transient AffinityKey<Long> key;
+
+ /**
+ * Default constructor.
+ */
+ public Person() {
+ // No-op.
+ }
+
+ /**
+ * Constructs person record.
+ *
+ * @param org Organization.
+ * @param firstName First name.
+ * @param lastName Last name.
+ * @param salary Salary.
+ * @param resume Resume text.
+ */
+ public Person(Organization org, String firstName, String lastName, double salary, String resume) {
+ // Generate unique ID for this person.
+ id = ID_GEN.incrementAndGet();
+
+ orgId = org.id();
+
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.salary = salary;
+ this.resume = resume;
+ }
+
+ /**
+ * Constructs person record.
+ *
+ * @param id Person ID.
+ * @param orgId Organization ID.
+ * @param firstName First name.
+ * @param lastName Last name.
+ * @param salary Salary.
+ * @param resume Resume text.
+ */
+ public Person(Long id, Long orgId, String firstName, String lastName, double salary, String resume) {
+ this.id = id;
+ this.orgId = orgId;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.salary = salary;
+ this.resume = resume;
+ }
+
+ /**
+ * Constructs person record.
+ *
+ * @param id Person ID.
+ * @param firstName First name.
+ * @param lastName Last name.
+ */
+ public Person(Long id, String firstName, String lastName) {
+ this.id = id;
+
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ /**
+ * Gets cache affinity key. Since in some examples person needs to be collocated with organization, we create
+ * custom affinity key to guarantee this collocation.
+ *
+ * @return Custom affinity key to guarantee that person is always collocated with organization.
+ */
+ public AffinityKey<Long> key() {
+ if (key == null)
+ key = new AffinityKey<>(id, orgId);
+
+ return key;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override public String toString() {
+ return "Person [id=" + id +
+ ", orgId=" + orgId +
+ ", lastName=" + lastName +
+ ", firstName=" + firstName +
+ ", salary=" + salary +
+ ", resume=" + resume + ']';
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/package-info.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/package-info.java
new file mode 100644
index 0000000..d6e1c4c
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/model/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * <!-- Package description. -->
+ * Model classes for Apache Ignite examples.
+ */
+
+package org.apache.ignite.examples.model;
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/DbH2ServerStartup.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/DbH2ServerStartup.java
new file mode 100644
index 0000000..f3da07d
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/DbH2ServerStartup.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.examples.util;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.sql.SQLException;
+import org.apache.ignite.IgniteException;
+import org.h2.jdbcx.JdbcConnectionPool;
+import org.h2.tools.RunScript;
+import org.h2.tools.Server;
+
+/**
+ * Start H2 database TCP server in order to access sample in-memory database from other processes.
+ */
+public class DbH2ServerStartup {
+ /** Create table script. */
+ private static final String CREATE_PERSON_TABLE =
+ "create table if not exists PERSON(id bigint not null, first_name varchar(50), last_name varchar(50), PRIMARY KEY(id));";
+
+ /** Sample data script. */
+ private static final String POPULATE_PERSON_TABLE =
+ "delete from PERSON;\n" +
+ "insert into PERSON(id, first_name, last_name) values(1, 'Johannes', 'Kepler');\n" +
+ "insert into PERSON(id, first_name, last_name) values(2, 'Galileo', 'Galilei');\n" +
+ "insert into PERSON(id, first_name, last_name) values(3, 'Henry', 'More');\n" +
+ "insert into PERSON(id, first_name, last_name) values(4, 'Polish', 'Brethren');\n" +
+ "insert into PERSON(id, first_name, last_name) values(5, 'Robert', 'Boyle');\n" +
+ "insert into PERSON(id, first_name, last_name) values(6, 'Wilhelm', 'Leibniz');";
+
+ /**
+ * Populate sample database.
+ *
+ * @throws SQLException if
+ */
+ public static void populateDatabase() throws SQLException {
+ // Try to connect to database TCP server.
+ JdbcConnectionPool dataSrc = JdbcConnectionPool.create("jdbc:h2:tcp://localhost/mem:ExampleDb", "sa", "");
+
+ // Create Person table in database.
+ RunScript.execute(dataSrc.getConnection(), new StringReader(CREATE_PERSON_TABLE));
+
+ // Populates Person table with sample data in database.
+ RunScript.execute(dataSrc.getConnection(), new StringReader(POPULATE_PERSON_TABLE));
+ }
+
+ /**
+ * Start H2 database TCP server.
+ *
+ * @param args Command line arguments, none required.
+ * @throws IgniteException If start H2 database TCP server failed.
+ */
+ public static void main(String[] args) throws IgniteException {
+ try {
+ // Start H2 database TCP server in order to access sample in-memory database from other processes.
+ Server.createTcpServer("-tcpDaemon").start();
+
+ populateDatabase();
+
+ // Try to connect to database TCP server.
+ JdbcConnectionPool dataSrc = JdbcConnectionPool.create("jdbc:h2:tcp://localhost/mem:ExampleDb", "sa", "");
+
+ // Create Person table in database.
+ RunScript.execute(dataSrc.getConnection(), new StringReader(CREATE_PERSON_TABLE));
+
+ // Populates Person table with sample data in database.
+ RunScript.execute(dataSrc.getConnection(), new StringReader(POPULATE_PERSON_TABLE));
+ }
+ catch (SQLException e) {
+ throw new IgniteException("Failed to start database TCP server", e);
+ }
+
+ try {
+ do {
+ System.out.println("Type 'q' and press 'Enter' to stop H2 TCP server...");
+ }
+ while ('q' != System.in.read());
+ }
+ catch (IOException ignored) {
+ // No-op.
+ }
+ }
+}
diff --git a/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/package-info.java b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/package-info.java
new file mode 100644
index 0000000..1d87d02
--- /dev/null
+++ b/modules/spring-tx-ext/examples/main/java/org/apache/ignite/examples/util/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * <!-- Package description. -->
+ * Contains utility classes for examples.
+ */
+package org.apache.ignite.examples.util;
diff --git a/modules/spring-tx-ext/pom.xml b/modules/spring-tx-ext/pom.xml
index 25f26f1..0bced6e 100644
--- a/modules/spring-tx-ext/pom.xml
+++ b/modules/spring-tx-ext/pom.xml
@@ -43,15 +43,16 @@
</dependency>
<dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-jdbc</artifactId>
+ <version>${spring.version}</version>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-spring</artifactId>
<version>${ignite.version}</version>
- <exclusions>
- <exclusion>
- <groupId>org.springframework</groupId>
- <artifactId>spring-tx</artifactId>
- </exclusion>
- </exclusions>
<scope>provided</scope>
</dependency>
@@ -125,13 +126,13 @@
<dependencies>
<dependency>
<groupId>org.apache.ignite</groupId>
- <artifactId>ignite-spring</artifactId>
+ <artifactId>ignite-core</artifactId>
<version>${ignite.version}</version>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
- <artifactId>ignite-core</artifactId>
+ <artifactId>ignite-spring</artifactId>
<version>${ignite.version}</version>
</dependency>
@@ -140,6 +141,12 @@
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-jdbc</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
</dependencies>
</profile>
</profiles>
diff --git a/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListener.java b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListener.java
new file mode 100644
index 0000000..d3a46d8
--- /dev/null
+++ b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListener.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.cache.store.spring;
+
+import javax.cache.integration.CacheWriterException;
+import javax.sql.DataSource;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.cache.store.CacheStoreSession;
+import org.apache.ignite.cache.store.CacheStoreSessionListener;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lifecycle.LifecycleAware;
+import org.apache.ignite.resources.LoggerResource;
+import org.apache.ignite.transactions.Transaction;
+import org.apache.ignite.transactions.TransactionIsolation;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+/**
+ * Cache store session listener based on Spring transaction management.
+ * <p>
+ * This listener starts a new DB transaction for each session and commits
+ * or rolls it back when session ends. If there is no ongoing
+ * cache transaction, this listener is no-op.
+ * <p>
+ * Store implementation can use any Spring APIs like {@link JdbcTemplate}
+ * and others. The listener will guarantee that if there is an
+ * ongoing cache transaction, all store operations within this
+ * transaction will be automatically enlisted in the same database
+ * transaction.
+ * <p>
+ * {@link org.apache.ignite.cache.store.spring.CacheSpringStoreSessionListener} requires that either
+ * {@link #setTransactionManager(PlatformTransactionManager) transaction manager}
+ * or {@link #setDataSource(DataSource) data source} is configured. If non of them is
+ * provided, exception is thrown. Is both are provided, data source will be
+ * ignored.
+ * <p>
+ * If there is a transaction, a {@link TransactionStatus} object will be saved
+ * as a store session {@link CacheStoreSession#attachment() attachment}. It
+ * can be used to acquire current DB transaction status.
+ */
+public class CacheSpringStoreSessionListener implements CacheStoreSessionListener, LifecycleAware {
+ /** Transaction manager. */
+ private PlatformTransactionManager txMgr;
+
+ /** Data source. */
+ private DataSource dataSrc;
+
+ /** Logger. */
+ @LoggerResource
+ private IgniteLogger log;
+
+ /**
+ * Sets transaction manager.
+ * <p>
+ * Either transaction manager or data source is required.
+ * If none is provided, exception will be thrown on startup.
+ *
+ * @param txMgr Transaction manager.
+ */
+ public void setTransactionManager(PlatformTransactionManager txMgr) {
+ this.txMgr = txMgr;
+ }
+
+ /**
+ * Gets transaction manager.
+ *
+ * @return Transaction manager.
+ */
+ public PlatformTransactionManager getTransactionManager() {
+ return txMgr;
+ }
+
+ /**
+ * Sets data source.
+ * <p>
+ * Either transaction manager or data source is required.
+ * If none is provided, exception will be thrown on startup.
+ *
+ * @param dataSrc Data source.
+ */
+ public void setDataSource(DataSource dataSrc) {
+ this.dataSrc = dataSrc;
+ }
+
+ /**
+ * Gets data source.
+ *
+ * @return Data source.
+ */
+ public DataSource getDataSource() {
+ return dataSrc;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void start() throws IgniteException {
+ if (txMgr == null && dataSrc == null)
+ throw new IgniteException("Either transaction manager or data source is required by " +
+ getClass().getSimpleName() + '.');
+
+ if (dataSrc != null) {
+ if (txMgr == null)
+ txMgr = new DataSourceTransactionManager(dataSrc);
+ else
+ U.warn(log, "Data source configured in " + getClass().getSimpleName() +
+ " will be ignored (transaction manager is already set).");
+ }
+
+ assert txMgr != null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void stop() throws IgniteException {
+ // No-op.
+ }
+
+ /** {@inheritDoc} */
+ @Override public void onSessionStart(CacheStoreSession ses) {
+ if (ses.isWithinTransaction() && ses.attachment() == null) {
+ try {
+ TransactionDefinition def = definition(ses.transaction(), ses.cacheName());
+
+ ses.attach(txMgr.getTransaction(def));
+ }
+ catch (TransactionException e) {
+ throw new CacheWriterException("Failed to start store session [tx=" + ses.transaction() + ']', e);
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public void onSessionEnd(CacheStoreSession ses, boolean commit) {
+ if (ses.isWithinTransaction()) {
+ TransactionStatus tx = ses.attach(null);
+
+ if (tx != null) {
+ try {
+ if (commit)
+ txMgr.commit(tx);
+ else
+ txMgr.rollback(tx);
+ }
+ catch (TransactionException e) {
+ throw new CacheWriterException("Failed to end store session [tx=" + ses.transaction() + ']', e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets DB transaction isolation level based on ongoing cache transaction isolation.
+ *
+ * @return DB transaction isolation.
+ */
+ private TransactionDefinition definition(Transaction tx, String cacheName) {
+ assert tx != null;
+
+ DefaultTransactionDefinition def = new DefaultTransactionDefinition();
+
+ def.setName("Ignite Tx [cache=" + (cacheName != null ? cacheName : "<default>") + ", id=" + tx.xid() + ']');
+ def.setIsolationLevel(isolationLevel(tx.isolation()));
+
+ long timeoutSec = (tx.timeout() + 500) / 1000;
+
+ if (timeoutSec > 0 && timeoutSec < Integer.MAX_VALUE)
+ def.setTimeout((int)timeoutSec);
+
+ return def;
+ }
+
+ /**
+ * Gets DB transaction isolation level based on ongoing cache transaction isolation.
+ *
+ * @param isolation Cache transaction isolation.
+ * @return DB transaction isolation.
+ */
+ private int isolationLevel(TransactionIsolation isolation) {
+ switch (isolation) {
+ case READ_COMMITTED:
+ return TransactionDefinition.ISOLATION_READ_COMMITTED;
+
+ case REPEATABLE_READ:
+ return TransactionDefinition.ISOLATION_REPEATABLE_READ;
+
+ case SERIALIZABLE:
+ return TransactionDefinition.ISOLATION_SERIALIZABLE;
+
+ default:
+ throw new IllegalStateException(); // Will never happen.
+ }
+ }
+}
diff --git a/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/package-info.java b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/package-info.java
new file mode 100644
index 0000000..408ba24
--- /dev/null
+++ b/modules/spring-tx-ext/src/main/java/org/apache/ignite/cache/store/spring/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Contains cache store session listener based on Spring transaction management.
+ */
+
+package org.apache.ignite.cache.store.spring;
diff --git a/modules/spring-tx-ext/src/test/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListenerSelfTest.java b/modules/spring-tx-ext/src/test/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListenerSelfTest.java
new file mode 100644
index 0000000..d3db02c
--- /dev/null
+++ b/modules/spring-tx-ext/src/test/java/org/apache/ignite/cache/store/spring/CacheSpringStoreSessionListenerSelfTest.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.cache.store.spring;
+
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import javax.cache.Cache;
+import javax.cache.configuration.Factory;
+import javax.cache.integration.CacheLoaderException;
+import javax.cache.integration.CacheWriterException;
+import javax.sql.DataSource;
+import org.apache.ignite.cache.store.CacheStore;
+import org.apache.ignite.cache.store.CacheStoreAdapter;
+import org.apache.ignite.cache.store.CacheStoreSession;
+import org.apache.ignite.cache.store.CacheStoreSessionListener;
+import org.apache.ignite.cache.store.CacheStoreSessionListenerAbstractSelfTest;
+import org.apache.ignite.cache.store.jdbc.CacheJdbcStoreSessionListener;
+import org.apache.ignite.internal.processors.query.h2.DistributedSqlConfiguration;
+import org.apache.ignite.internal.processors.query.h2.FunctionsManager;
+import org.apache.ignite.lang.IgniteBiInClosure;
+import org.apache.ignite.resources.CacheStoreSessionResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DataSourceUtils;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.transaction.TransactionStatus;
+
+/**
+ * Tests for {@link CacheJdbcStoreSessionListener}.
+ */
+public class CacheSpringStoreSessionListenerSelfTest extends CacheStoreSessionListenerAbstractSelfTest {
+ /** */
+ private static final DataSource DATA_SRC = new DriverManagerDataSource(URL);
+
+ /** */
+ private static Method FunctionManager_removeFunctions;
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ FunctionManager_removeFunctions = FunctionsManager.class.getDeclaredMethod("removeFunctions", Set.class);
+
+ FunctionManager_removeFunctions.setAccessible(true);
+
+ // Cleanup disabled functions because transaction manager uses LOCK_MODE()
+ FunctionManager_removeFunctions.invoke(FunctionsManager.class, Collections.emptySet());
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() throws Exception {
+ FunctionManager_removeFunctions.invoke(FunctionsManager.class, DistributedSqlConfiguration.DFLT_DISABLED_FUNCS);
+
+ super.afterTestsStopped();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Factory<? extends CacheStore<Integer, Integer>> storeFactory() {
+ return new Factory<CacheStore<Integer, Integer>>() {
+ @Override public CacheStore<Integer, Integer> create() {
+ return new Store(new JdbcTemplate(DATA_SRC));
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Factory<CacheStoreSessionListener> sessionListenerFactory() {
+ return new Factory<CacheStoreSessionListener>() {
+ @Override public CacheStoreSessionListener create() {
+ CacheSpringStoreSessionListener lsnr = new CacheSpringStoreSessionListener();
+
+ lsnr.setDataSource(DATA_SRC);
+
+ return lsnr;
+ }
+ };
+ }
+
+ /**
+ */
+ private static class Store extends CacheStoreAdapter<Integer, Integer> {
+ /** */
+ private static String SES_CONN_KEY = "ses_conn";
+
+ /** */
+ private final JdbcTemplate jdbc;
+
+ /** */
+ @CacheStoreSessionResource
+ private CacheStoreSession ses;
+
+ /**
+ * @param jdbc JDBC template.
+ */
+ private Store(JdbcTemplate jdbc) {
+ this.jdbc = jdbc;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void loadCache(IgniteBiInClosure<Integer, Integer> clo, Object... args) {
+ loadCacheCnt.incrementAndGet();
+
+ checkTransaction();
+ checkConnection();
+ }
+
+ /** {@inheritDoc} */
+ @Override public Integer load(Integer key) throws CacheLoaderException {
+ loadCnt.incrementAndGet();
+
+ checkTransaction();
+ checkConnection();
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void write(Cache.Entry<? extends Integer, ? extends Integer> entry)
+ throws CacheWriterException {
+ writeCnt.incrementAndGet();
+
+ checkTransaction();
+ checkConnection();
+
+ if (write.get()) {
+ String table;
+
+ switch (ses.cacheName()) {
+ case "cache1":
+ table = "Table1";
+
+ break;
+
+ case "cache2":
+ if (fail.get())
+ throw new CacheWriterException("Expected failure.");
+
+ table = "Table2";
+
+ break;
+
+ default:
+ throw new CacheWriterException("Wring cache: " + ses.cacheName());
+ }
+
+ jdbc.update("INSERT INTO " + table + " (key, value) VALUES (?, ?)",
+ entry.getKey(), entry.getValue());
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(Object key) throws CacheWriterException {
+ deleteCnt.incrementAndGet();
+
+ checkTransaction();
+ checkConnection();
+ }
+
+ /** {@inheritDoc} */
+ @Override public void sessionEnd(boolean commit) {
+ assertNull(ses.attachment());
+ }
+
+ /**
+ */
+ private void checkTransaction() {
+ TransactionStatus tx = ses.attachment();
+
+ if (ses.isWithinTransaction()) {
+ assertNotNull(tx);
+ assertFalse(tx.isCompleted());
+ }
+ else
+ assertNull(tx);
+ }
+
+ /**
+ */
+ private void checkConnection() {
+ Connection conn = DataSourceUtils.getConnection(jdbc.getDataSource());
+
+ assertNotNull(conn);
+
+ try {
+ assertFalse(conn.isClosed());
+ assertEquals(!ses.isWithinTransaction(), conn.getAutoCommit());
+ }
+ catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+
+ verifySameInstance(conn);
+ }
+
+ /**
+ * @param conn Connection.
+ */
+ private void verifySameInstance(Connection conn) {
+ Map<String, Connection> props = ses.properties();
+
+ Connection sesConn = props.get(SES_CONN_KEY);
+
+ if (sesConn == null)
+ props.put(SES_CONN_KEY, conn);
+ else {
+ assertSame(conn, sesConn);
+
+ reuseCnt.incrementAndGet();
+ }
+ }
+ }
+}