IGNITE-13559 Migrates spring-data modules to ignite-extensions. (#25)
diff --git a/.gitignore b/.gitignore
index 3a94647..82018b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,3 +142,7 @@
.history
.vscode
.classpath
+
+# Ignite work folder
+**/ignite/work/
+**/ignite/README.txt
diff --git a/modules/spring-data-2.0-ext/README.txt b/modules/spring-data-2.0-ext/README.txt
new file mode 100644
index 0000000..001ec34
--- /dev/null
+++ b/modules/spring-data-2.0-ext/README.txt
@@ -0,0 +1,43 @@
+Apache Ignite Spring Module
+---------------------------
+
+Apache Ignite Spring Data 2.0 extension provides an integration with Spring Data 2.0 framework.
+
+Main features:
+
+- Supports multiple Ignite instances on same JVM (@RepositoryConfig).
+- Supports query tuning parameters in @Query annotation
+- Supports projections
+- Supports Page and Stream responses
+- Supports Sql Fields Query resultset transformation into the domain entity
+- Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam")
+- Supports advanced parameter binding and SpEL expressions into SQL queries:
+- Template variables:
+ - #entityName - the simple class name of the domain entity
+- Method parameter expressions: Parameters are exposed for indexed access ([0] is the first query method's param) or via the name declared using @Param. The actual SpEL expression binding is triggered by ?#. Example: ?#{[0] or ?#{#myParamName}
+- Advanced SpEL expressions: While advanced parameter binding is a very useful feature, the real power of SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components through SpEL EvaluationContext extension model.
+- Supports SpEL expressions into Text queries (TextQuery).
+
+Importing Spring Data 2.0 extension In Maven Project
+----------------------------------------
+
+If you are using Maven to manage dependencies of your project, you can add Spring Data 2.0 extension
+dependency like this (replace '${ignite-spring-data_2.0-ext.version}' with actual version of Ignite Spring Data 2.0
+extension you are interested in):
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ ...
+ <dependencies>
+ ...
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring-data_2.0-ext</artifactId>
+ <version>${ignite-spring-data_2.0-ext.version}</version>
+ </dependency>
+ ...
+ </dependencies>
+ ...
+</project>
diff --git a/modules/spring-data-2.0-ext/examples/config/example-default.xml b/modules/spring-data-2.0-ext/examples/config/example-default.xml
new file mode 100644
index 0000000..e6c359d
--- /dev/null
+++ b/modules/spring-data-2.0-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-data-2.0-ext/examples/config/example-spring-data.xml b/modules/spring-data-2.0-ext/examples/config/example-spring-data.xml
new file mode 100644
index 0000000..4f51c12
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/config/example-spring-data.xml
@@ -0,0 +1,64 @@
+<?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 Spring configuration file to startup Ignite cache.
+
+ This file demonstrates how to configure cache using Spring. Provided cache
+ will be created on node startup.
+
+ Use this configuration file when running Spring Data examples.
+
+ When starting a standalone node, you need to execute the following command:
+ {IGNITE_HOME}/bin/ignite.{bat|sh} modules/spring-data-2.0/examples/config/example-spring-data.xml
+
+ When starting Ignite from Java IDE, pass path to this file to Ignition:
+ Ignition.start("modules/spring-data-2.0/examples/config/example-spring-data.xml");
+-->
+<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">
+ <property name="igniteInstanceName" value="springDataNode" />
+
+ <property name="cacheConfiguration">
+ <list>
+ <bean class="org.apache.ignite.configuration.CacheConfiguration">
+ <!--
+ Apache Ignite uses an IgniteRepository extension which inherits from Spring Data's CrudRepository.
+ The SQL grid is also enabled to aceess Spring Data repository. The @RepositoryConfig annotation
+ maps the PersonRepository to an Ignite's cache named "PersonCache".
+ -->
+ <property name="name" value="PersonCache"/>
+ <property name="indexedTypes">
+ <list>
+ <value>java.lang.Long</value>
+ <value>org.apache.ignite.springdata20.examples.model.Person</value>
+ </list>
+ </property>
+ </bean>
+ </list>
+ </property>
+ </bean>
+</beans>
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/PersonRepository.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/PersonRepository.java
new file mode 100644
index 0000000..2e0a729
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/PersonRepository.java
@@ -0,0 +1,59 @@
+/*
+ * 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.springdata20.examples;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.examples.model.Person;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+
+/**
+ * Apache Ignite Spring Data repository backed by Ignite Person's cache.
+ * </p>
+ * To link the repository with an Ignite cache use {@link RepositoryConfig#cacheName()} annotation's parameter.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Long> {
+ /**
+ * Gets all the persons with the given name.
+ * @param name Person name.
+ * @return A list of Persons with the given first name.
+ */
+ public List<Person> findByFirstName(String name);
+
+ /**
+ * Returns top Person with the specified surname.
+ * @param name Person surname.
+ * @return Person that satisfy the query.
+ */
+ public Cache.Entry<Long, Person> findTopByLastNameLike(String name);
+
+ /**
+ * Getting ids of all the Person satisfying the custom query from {@link Query} annotation.
+ *
+ * @param orgId Query parameter.
+ * @param pageable Pageable interface.
+ * @return A list of Persons' ids.
+ */
+ @Query("SELECT id FROM Person WHERE orgId > ?")
+ public List<Long> selectId(long orgId, Pageable pageable);
+}
+
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringApplicationConfiguration.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringApplicationConfiguration.java
new file mode 100644
index 0000000..d7b4a91
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringApplicationConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * 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.springdata20.examples;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.EnableIgniteRepositories;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Every {@link IgniteRepository} is bound to a specific Apache Ignite that it communicates to in order to mutate and
+ * read data via Spring Data API. To pass an instance of Apache Ignite cache to an {@link IgniteRepository} it's
+ * required to initialize {@link IgniteRepositoryFactoryBean} with on of the following:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance"</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg"</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath"</li>
+ * <ul/>
+ * In this example the first approach is utilized.
+ */
+@Configuration
+@EnableIgniteRepositories
+public class SpringApplicationConfiguration {
+ /**
+ * Creating Apache Ignite instance bean. A bean will be passed to {@link IgniteRepositoryFactoryBean} to initialize
+ * all Ignite based Spring Data repositories and connect to a cluster.
+ */
+ @Bean
+ public Ignite igniteInstance() {
+ return Ignition.start("modules/spring-data-2.0-ext/examples/config/example-spring-data.xml");
+ }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringDataExample.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringDataExample.java
new file mode 100644
index 0000000..a3d5547
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringDataExample.java
@@ -0,0 +1,150 @@
+/*
+ * 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.springdata20.examples;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeMap;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.examples.model.Person;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+
+/**
+ * The example demonstrates how to interact with an Apache Ignite cluster by means of Spring Data API.
+ *
+ * Additional cluster nodes can be started with special configuration file which
+ * enables P2P class loading: {@code 'ignite.{sh|bat} modules/spring-data-2.0/examples/config/example-spring-data.xml'}.
+ */
+public class SpringDataExample {
+ /** Spring Application Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** Ignite Spring Data repository. */
+ private static PersonRepository repo;
+
+ /**
+ * Executes the example.
+ * @param args Command line arguments, none required.
+ */
+ public static void main(String[] args) {
+ // Initializing Spring Data context and Ignite repository.
+ igniteSpringDataInit();
+
+ populateRepository();
+
+ findPersons();
+
+ queryRepository();
+
+ System.out.println("\n>>> Cleaning out the repository...");
+
+ repo.deleteAll();
+
+ System.out.println("\n>>> Repository size: " + repo.count());
+
+ // Destroying the context.
+ ctx.destroy();
+ }
+
+ /**
+ * Initializes Spring Data and Ignite repositories.
+ */
+ private static void igniteSpringDataInit() {
+ ctx = new AnnotationConfigApplicationContext();
+
+ // Explicitly registering Spring configuration.
+ ctx.register(SpringApplicationConfiguration.class);
+
+ ctx.refresh();
+
+ // Getting a reference to PersonRepository.
+ repo = ctx.getBean(PersonRepository.class);
+ }
+
+ /**
+ * Fills the repository in with sample data.
+ */
+ private static void populateRepository() {
+ TreeMap<Long, Person> persons = new TreeMap<>();
+
+ persons.put(1L, new Person(1L, 2000L, "John", "Smith", 15000, "Worked for Apple"));
+ persons.put(2L, new Person(2L, 2000L, "Brad", "Pitt", 16000, "Worked for Oracle"));
+ persons.put(3L, new Person(3L, 1000L, "Mark", "Tomson", 10000, "Worked for Sun"));
+ persons.put(4L, new Person(4L, 2000L, "Erick", "Smith", 13000, "Worked for Apple"));
+ persons.put(5L, new Person(5L, 1000L, "John", "Rozenberg", 25000, "Worked for RedHat"));
+ persons.put(6L, new Person(6L, 2000L, "Denis", "Won", 35000, "Worked for CBS"));
+ persons.put(7L, new Person(7L, 1000L, "Abdula", "Adis", 45000, "Worked for NBC"));
+ persons.put(8L, new Person(8L, 2000L, "Roman", "Ive", 15000, "Worked for Sun"));
+
+ // Adding data into the repository.
+ repo.save(persons);
+
+ System.out.println("\n>>> Added " + repo.count() + " Persons into the repository.");
+ }
+
+ /**
+ * Gets a list of Persons using standard read operations.
+ */
+ private static void findPersons() {
+ // Getting Person with specific ID.
+ Person person = repo.findById(2L).orElse(null);
+
+ System.out.println("\n>>> Found Person [id=" + 2L + ", val=" + person + "]");
+
+ // Getting a list of Persons.
+
+ ArrayList<Long> ids = new ArrayList<>();
+
+ for (long i = 0; i < 5; i++)
+ ids.add(i);
+
+ Iterator<Person> persons = repo.findAllById(ids).iterator();
+
+ System.out.println("\n>>> Persons list for specific ids: ");
+
+ while (persons.hasNext())
+ System.out.println(" >>> " + persons.next());
+ }
+
+ /**
+ * Execute advanced queries over the repository.
+ */
+ private static void queryRepository() {
+ System.out.println("\n>>> Persons with name 'John':");
+
+ List<Person> persons = repo.findByFirstName("John");
+
+ for (Person person: persons)
+ System.out.println(" >>> " + person);
+
+
+ Cache.Entry<Long, Person> topPerson = repo.findTopByLastNameLike("Smith");
+
+ System.out.println("\n>>> Top Person with surname 'Smith': " + topPerson.getValue());
+
+
+ List<Long> ids = repo.selectId(1000L, new PageRequest(0, 4));
+
+ System.out.println("\n>>> Persons working for organization with ID > 1000: ");
+
+ for (Long id: ids)
+ System.out.println(" >>> [id=" + id + "]");
+ }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Address.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Address.java
new file mode 100644
index 0000000..e7e6348
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/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.springdata20.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 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-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Employee.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Employee.java
new file mode 100644
index 0000000..7996793
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/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.springdata20.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-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/EmployeeKey.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/EmployeeKey.java
new file mode 100644
index 0000000..5d16ebd
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/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.springdata20.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-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Organization.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Organization.java
new file mode 100644
index 0000000..d06e05c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/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.springdata20.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-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/OrganizationType.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/OrganizationType.java
new file mode 100644
index 0000000..7bcb0c2
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/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.springdata20.examples.model;
+
+/**
+ * Organization type enum.
+ */
+public enum OrganizationType {
+ /** Non-profit organization. */
+ NON_PROFIT,
+
+ /** Private organization. */
+ PRIVATE,
+
+ /** Government organization. */
+ GOVERNMENT
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Person.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Person.java
new file mode 100644
index 0000000..e9250be
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/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.springdata20.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-data-2.0-ext/licenses/apache-2.0.txt b/modules/spring-data-2.0-ext/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/spring-data-2.0-ext/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/modules/spring-data-2.0-ext/modules/core/src/test/config/log4j-test.xml b/modules/spring-data-2.0-ext/modules/core/src/test/config/log4j-test.xml
new file mode 100755
index 0000000..3061bd4
--- /dev/null
+++ b/modules/spring-data-2.0-ext/modules/core/src/test/config/log4j-test.xml
@@ -0,0 +1,97 @@
+<?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.
+-->
+
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+ "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+<!--
+ Log4j configuration.
+-->
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+ <!--
+ Logs System.out messages to console.
+ -->
+ <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+ <!-- Log to STDOUT. -->
+ <param name="Target" value="System.out"/>
+
+ <!-- Log from DEBUG and higher. -->
+ <param name="Threshold" value="DEBUG"/>
+
+ <!-- The default pattern: Date Priority [Category] Message\n -->
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+
+ <!-- Do not log beyond INFO level. -->
+ <filter class="org.apache.log4j.varia.LevelRangeFilter">
+ <param name="levelMin" value="DEBUG"/>
+ <param name="levelMax" value="INFO"/>
+ </filter>
+ </appender>
+
+ <!--
+ Logs all System.err messages to console.
+ -->
+ <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+ <!-- Log to STDERR. -->
+ <param name="Target" value="System.err"/>
+
+ <!-- Log from WARN and higher. -->
+ <param name="Threshold" value="WARN"/>
+
+ <!-- The default pattern: Date Priority [Category] Message\n -->
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+ </appender>
+
+ <!--
+ Logs all output to specified file.
+ -->
+ <appender name="FILE" class="org.apache.log4j.RollingFileAppender">
+ <param name="Threshold" value="DEBUG"/>
+ <param name="File" value="ignite/work/log/ignite.log"/>
+ <param name="Append" value="true"/>
+ <param name="MaxFileSize" value="10MB"/>
+ <param name="MaxBackupIndex" value="10"/>
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+ </appender>
+
+ <!-- Disable all open source debugging. -->
+ <category name="org">
+ <level value="INFO"/>
+ </category>
+
+ <category name="org.eclipse.jetty">
+ <level value="INFO"/>
+ </category>
+
+ <!-- Default settings. -->
+ <root>
+ <!-- Print at info by default. -->
+ <level value="INFO"/>
+
+ <!-- Append to file and console. -->
+ <appender-ref ref="FILE"/>
+ <appender-ref ref="CONSOLE"/>
+ <appender-ref ref="CONSOLE_ERR"/>
+ </root>
+</log4j:configuration>
diff --git a/modules/spring-data-2.0-ext/modules/core/src/test/config/tests.properties b/modules/spring-data-2.0-ext/modules/core/src/test/config/tests.properties
new file mode 100644
index 0000000..0faf5b8
--- /dev/null
+++ b/modules/spring-data-2.0-ext/modules/core/src/test/config/tests.properties
@@ -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.
+#
+
+# Local address to bind to.
+local.ip=127.0.0.1
+
+# TCP communication port
+comm.tcp.port=30010
diff --git a/modules/spring-data-2.0-ext/pom.xml b/modules/spring-data-2.0-ext/pom.xml
new file mode 100644
index 0000000..00f7fa5
--- /dev/null
+++ b/modules/spring-data-2.0-ext/pom.xml
@@ -0,0 +1,182 @@
+<?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.
+-->
+
+<!--
+ POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-extensions-parent</artifactId>
+ <version>1</version>
+ <relativePath>../../parent</relativePath>
+ </parent>
+
+ <artifactId>ignite-spring-data_2.0-ext</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <url>http://ignite.apache.org</url>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-indexing</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-log4j</artifactId>
+ <version>${ignite.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.data</groupId>
+ <artifactId>spring-data-commons</artifactId>
+ <version>${spring.data-2.0.version}</version>
+ <!-- Exclude slf4j logging in favor of log4j -->
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring</artifactId>
+ <version>${ignite.version}</version>
+ <!--Remove exclusion while upgrading ignite-spring version to 5.0-->
+ <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>
+ <version>${spring-5.0.version}</version>
+ </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>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-tools</artifactId>
+ <version>${ignite.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>examples</id>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>add-source</goal>
+ </goals>
+ <configuration>
+ <sources>
+ <source>examples/main/java</source>
+ </sources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+</project>
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/IgniteRepository.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/IgniteRepository.java
new file mode 100644
index 0000000..8ba8de6
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/IgniteRepository.java
@@ -0,0 +1,109 @@
+/*
+ * 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.springdata20.repository;
+
+import java.io.Serializable;
+import java.util.Map;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Apache Ignite repository that extends basic capabilities of {@link CrudRepository}.
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public interface IgniteRepository<V, K extends Serializable> extends CrudRepository<V, K> {
+ /**
+ * Returns the Ignite instance bound to the repository
+ *
+ * @return the Ignite instance bound to the repository
+ */
+ public Ignite ignite();
+
+ /**
+ * Returns the Ignite Cache bound to the repository
+ *
+ * @return the Ignite Cache bound to the repository
+ */
+ public IgniteCache<K, V> cache();
+
+ /**
+ * Saves a given entity using provided key.
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Entity type.
+ * @param key Entity's key.
+ * @param entity Entity to save.
+ * @return Saved entity.
+ */
+ public <S extends V> S save(K key, S entity);
+
+ /**
+ * Saves all given keys and entities combinations.
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Type of entities.
+ * @param entities Map of key-entities pairs to save.
+ * @return Saved entities.
+ */
+ public <S extends V> Iterable<S> save(Map<K, S> entities);
+
+ /**
+ * Saves a given entity using provided key with expiry policy
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Entity type.
+ * @param key Entity's key.
+ * @param entity Entity to save.
+ * @param expiryPlc ExpiryPolicy to apply, if not null.
+ * @return Saved entity.
+ */
+ public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc);
+
+ /**
+ * Saves all given keys and entities combinations with expiry policy
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Type of entities.
+ * @param entities Map of key-entities pairs to save.
+ * @param expiryPlc ExpiryPolicy to apply, if not null.
+ * @return Saved entities.
+ */
+ public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc);
+
+ /**
+ * Deletes all the entities for the provided ids.
+ *
+ * @param ids List of ids to delete.
+ */
+ public void deleteAllById(Iterable<K> ids);
+
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/DynamicQueryConfig.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/DynamicQueryConfig.java
new file mode 100644
index 0000000..ed422a9
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/DynamicQueryConfig.java
@@ -0,0 +1,348 @@
+/*
+ * 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.springdata20.repository.config;
+
+/**
+ * Runtime Dynamic query configuration.
+ * <p>
+ * Can be used as special repository method parameter to provide at runtime:
+ * <ol>
+ * <li>Dynamic query string (requires {@link Query#dynamicQuery()} == true)
+ * <li>Ignite query tuning*
+ * </ol>
+ * <p>
+ * * Please, note that {@link Query} annotation parameters will be ignored in favor of those defined in
+ * {@link DynamicQueryConfig} parameter if present.
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class DynamicQueryConfig {
+ /** */
+ private String value = "";
+
+ /** */
+ private boolean textQuery;
+
+ /** */
+ private boolean forceFieldsQry;
+
+ /** */
+ private boolean collocated;
+
+ /** */
+ private int timeout;
+
+ /** */
+ private boolean enforceJoinOrder;
+
+ /** */
+ private boolean distributedJoins;
+
+ /** */
+ private boolean lazy;
+
+ /** */
+ private boolean local;
+
+ /** */
+ private int[] parts;
+
+ /** */
+ private int limit;
+
+ /**
+ * From Query annotation.
+ *
+ * @param queryConfiguration the query configuration
+ * @return the dynamic query config
+ */
+ public static DynamicQueryConfig fromQueryAnnotation(Query queryConfiguration) {
+ DynamicQueryConfig config = new DynamicQueryConfig();
+ if (queryConfiguration != null) {
+ config.value = queryConfiguration.value();
+ config.collocated = queryConfiguration.collocated();
+ config.timeout = queryConfiguration.timeout();
+ config.enforceJoinOrder = queryConfiguration.enforceJoinOrder();
+ config.distributedJoins = queryConfiguration.distributedJoins();
+ config.lazy = queryConfiguration.lazy();
+ config.parts = queryConfiguration.parts();
+ config.local = queryConfiguration.local();
+ config.limit = queryConfiguration.limit();
+ }
+ return config;
+ }
+
+ /**
+ * Query text string.
+ *
+ * @return the string
+ */
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Whether must use TextQuery search.
+ *
+ * @return the boolean
+ */
+ public boolean textQuery() {
+ return textQuery;
+ }
+
+ /**
+ * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+ * statements or to not return hidden fields on SELECT * statements.
+ *
+ * @return the boolean
+ */
+ public boolean forceFieldsQuery() {
+ return forceFieldsQry;
+ }
+
+ /**
+ * Sets flag defining if this query is collocated.
+ * <p>
+ * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+ * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+ * of your query selection are collocated together on the same node and you group by collocated key (primary or
+ * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+ * nodes.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ *
+ * @return the boolean
+ */
+ public boolean collocated() {
+ return collocated;
+ }
+
+ /**
+ * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+ * timeout is exceeded. Zero value disables timeout
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ *
+ * @return the int
+ */
+ public int timeout() {
+ return timeout;
+ }
+
+ /**
+ * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+ * tables in join. By default is {@code false}.
+ * <p>
+ * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+ * correct and tuned as much as possible but query optimizer still produces wrong join order.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ *
+ * @return the boolean
+ */
+ public boolean enforceJoinOrder() {
+ return enforceJoinOrder;
+ }
+
+ /**
+ * Specify if distributed joins are enabled for this query.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ *
+ * @return the boolean
+ */
+ public boolean distributedJoins() {
+ return distributedJoins;
+ }
+
+ /**
+ * Sets lazy query execution flag.
+ * <p>
+ * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+ * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+ * increasing concurrency.
+ * <p>
+ * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+ * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+ * consumption at the cost of moderate performance hit.
+ * <p>
+ * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ *
+ * @return the boolean
+ */
+ public boolean lazy() {
+ return lazy;
+ }
+
+ /**
+ * Sets whether this query should be executed on local node only.
+ *
+ * @return the boolean
+ */
+ public boolean local() {
+ return local;
+ }
+
+ /**
+ * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+ * partitions.
+ * <p>
+ * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ *
+ * @return the int [ ]
+ */
+ public int[] parts() {
+ return parts;
+ }
+
+ /**
+ * Gets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+ *
+ * @return Limit value.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Sets value.
+ *
+ * @param value the value
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setValue(String value) {
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * Sets text query.
+ *
+ * @param textQuery the text query
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setTextQuery(boolean textQuery) {
+ this.textQuery = textQuery;
+ return this;
+ }
+
+ /**
+ * Sets force fields query.
+ *
+ * @param forceFieldsQuery the force fields query
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setForceFieldsQuery(boolean forceFieldsQuery) {
+ forceFieldsQry = forceFieldsQuery;
+ return this;
+ }
+
+ /**
+ * Sets collocated.
+ *
+ * @param collocated the collocated
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setCollocated(boolean collocated) {
+ this.collocated = collocated;
+ return this;
+ }
+
+ /**
+ * Sets timeout.
+ *
+ * @param timeout the timeout
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setTimeout(int timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Sets enforce join order.
+ *
+ * @param enforceJoinOrder the enforce join order
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setEnforceJoinOrder(boolean enforceJoinOrder) {
+ this.enforceJoinOrder = enforceJoinOrder;
+ return this;
+ }
+
+ /**
+ * Sets distributed joins.
+ *
+ * @param distributedJoins the distributed joins
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setDistributedJoins(boolean distributedJoins) {
+ this.distributedJoins = distributedJoins;
+ return this;
+ }
+
+ /**
+ * Sets lazy.
+ *
+ * @param lazy the lazy
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setLazy(boolean lazy) {
+ this.lazy = lazy;
+ return this;
+ }
+
+ /**
+ * Sets local.
+ *
+ * @param local the local
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setLocal(boolean local) {
+ this.local = local;
+ return this;
+ }
+
+ /**
+ * Sets parts.
+ *
+ * @param parts the parts
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setParts(int[] parts) {
+ this.parts = parts;
+ return this;
+ }
+
+ /**
+ * Sets limit to response records count for TextQuery.
+ *
+ * @param limit If 0 or less, considered to be no limit.
+ * @return {@code this} For chaining.
+ */
+ public DynamicQueryConfig setLimit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/EnableIgniteRepositories.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/EnableIgniteRepositories.java
new file mode 100644
index 0000000..1bab4ca
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/EnableIgniteRepositories.java
@@ -0,0 +1,119 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactoryBean;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryImpl;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate Apache Ignite repositories. If no base package is configured through either {@link #value()},
+ * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(IgniteRepositoriesRegistar.class)
+public @interface EnableIgniteRepositories {
+ /**
+ * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
+ * {@code @EnableIgniteRepositories("org.my.pkg")} instead of
+ * {@code @EnableIgniteRepositories(basePackages="org.my.pkg")}.
+ */
+ String[] value() default {};
+
+ /**
+ * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with)
+ * this attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+ */
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components.
+ * The package of each class specified will be scanned. Consider creating a special no-op marker class or interface
+ * in each package that serves no purpose other than being referenced by this attribute.
+ */
+ Class<?>[] basePackageClasses() default {};
+
+ /**
+ * Specifies which types are not eligible for component scanning.
+ */
+ Filter[] excludeFilters() default {};
+
+ /**
+ * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+ * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or
+ * filters.
+ */
+ Filter[] includeFilters() default {};
+
+ /**
+ * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+ * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+ * for {@code PersonRepositoryImpl}.
+ *
+ * @return Postfix to be used when looking up custom repository implementations.
+ */
+ String repositoryImplementationPostfix() default "Impl";
+
+ /**
+ * Configures the location of where to find the Spring Data named queries properties file.
+ *
+ * @return Location of where to find the Spring Data named queries properties file.
+ */
+ String namedQueriesLocation() default "";
+
+ /**
+ * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+ * {@link Key#CREATE_IF_NOT_FOUND}.
+ *
+ * @return Key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods.
+ */
+ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+ /**
+ * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to {@link
+ * IgniteRepositoryFactoryBean}.
+ *
+ * @return {@link FactoryBean} class to be used for each repository instance.
+ */
+ Class<?> repositoryFactoryBeanClass() default IgniteRepositoryFactoryBean.class;
+
+ /**
+ * Configure the repository base class to be used to create repository proxies for this particular configuration.
+ *
+ * @return Repository base class to be used to create repository proxies for this particular configuration.
+ */
+ Class<?> repositoryBaseClass() default IgniteRepositoryImpl.class;
+
+ /**
+ * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+ * repositories infrastructure.
+ */
+ boolean considerNestedRepositories() default false;
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoriesRegistar.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoriesRegistar.java
new file mode 100644
index 0000000..83ff7ff
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoriesRegistar.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.springdata20.repository.config;
+
+import java.lang.annotation.Annotation;
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryBeanDefinitionRegistrarSupport}.
+ */
+public class IgniteRepositoriesRegistar extends RepositoryBeanDefinitionRegistrarSupport {
+ /** {@inheritDoc} */
+ @Override protected Class<? extends Annotation> getAnnotation() {
+ return EnableIgniteRepositories.class;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryConfigurationExtension getExtension() {
+ return new IgniteRepositoryConfigurationExtension();
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoryConfigurationExtension.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoryConfigurationExtension.java
new file mode 100644
index 0000000..354e35b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoryConfigurationExtension.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryConfigurationExtension}.
+ */
+public class IgniteRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
+ /** {@inheritDoc} */
+ @Override public String getModuleName() {
+ return "Apache Ignite";
+ }
+
+ /** {@inheritDoc} */
+ @Override protected String getModulePrefix() {
+ return "ignite";
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getRepositoryFactoryBeanClassName() {
+ return IgniteRepositoryFactoryBean.class.getName();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Collection<Class<?>> getIdentifyingTypes() {
+ return Collections.singleton(IgniteRepository.class);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/Query.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/Query.java
new file mode 100644
index 0000000..7b44d19
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/Query.java
@@ -0,0 +1,137 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to provide a user defined query for a method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Query {
+ /**
+ * Query text string. If not provided, Ignite query generator for Spring Data framework will be used to generate one
+ * (only if textQuery = false (default))
+ */
+ String value() default "";
+
+ /**
+ * Whether annotated repository method must use TextQuery search.
+ */
+ boolean textQuery() default false;
+
+ /**
+ * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+ * statements or to not return hidden fields on SELECT * statements.
+ */
+ boolean forceFieldsQuery() default false;
+
+ /**
+ * Sets flag defining if this query is collocated.
+ * <p>
+ * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+ * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+ * of your query selection are collocated together on the same node and you group by collocated key (primary or
+ * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+ * nodes.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ */
+ boolean collocated() default false;
+
+ /**
+ * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+ * timeout is exceeded. Zero value disables timeout
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ */
+ int timeout() default 0;
+
+ /**
+ * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+ * tables in join. By default is {@code false}.
+ * <p>
+ * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+ * correct and tuned as much as possible but query optimizer still produces wrong join order.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ */
+ boolean enforceJoinOrder() default false;
+
+ /**
+ * Specify if distributed joins are enabled for this query.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ */
+ boolean distributedJoins() default false;
+
+ /**
+ * Sets lazy query execution flag.
+ * <p>
+ * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+ * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+ * increasing concurrency.
+ * <p>
+ * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+ * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+ * consumption at the cost of moderate performance hit.
+ * <p>
+ * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ */
+ boolean lazy() default false;
+
+ /**
+ * Sets whether this query should be executed on local node only.
+ */
+ boolean local() default false;
+
+ /**
+ * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+ * partitions.
+ * <p>
+ * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ */
+ int[] parts() default {};
+
+ /**
+ * Specify whether the annotated method must provide a non null {@link DynamicQueryConfig} parameter with a non
+ * empty value (query string) or {@link DynamicQueryConfig#textQuery()} == true.
+ * <p>
+ * Please, note that {@link DynamicQueryConfig#textQuery()} annotation parameters will be ignored in favor of those
+ * defined in {@link DynamicQueryConfig} parameter if present (runtime ignite query tuning).
+ */
+ boolean dynamicQuery() default false;
+
+ /**
+ * Sets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+ */
+ int limit() default 0;
+
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/RepositoryConfig.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/RepositoryConfig.java
new file mode 100644
index 0000000..359b75b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/RepositoryConfig.java
@@ -0,0 +1,75 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+
+/**
+ * The annotation can be used to pass Ignite specific parameters to a bound repository.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepositoryConfig {
+ /**
+ * Cache name string.
+ *
+ * @return A name of a distributed Apache Ignite cache an annotated repository will be mapped to.
+ */
+ String cacheName() default "";
+
+ /**
+ * Ignite instance string. Default "igniteInstance".
+ *
+ * @return {@link Ignite} instance spring bean name
+ */
+ String igniteInstance() default "igniteInstance";
+
+ /**
+ * Ignite cfg string. Default "igniteCfg".
+ *
+ * @return {@link IgniteConfiguration} spring bean name
+ */
+ String igniteCfg() default "igniteCfg";
+
+ /**
+ * Ignite spring cfg path string. Default "igniteSpringCfgPath".
+ *
+ * @return A path to Ignite's Spring XML configuration spring bean name
+ */
+ String igniteSpringCfgPath() default "igniteSpringCfgPath";
+
+ /**
+ * Auto create cache. Default false to enforce control over cache creation and to avoid cache creation by mistake
+ * <p>
+ * Tells to Ignite Repository factory wether cache should be auto created if not exists.
+ *
+ * @return the boolean
+ */
+ boolean autoCreateCache() default false;
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/package-info.java
new file mode 100644
index 0000000..1c7b2f9
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/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. -->
+ * Package includes Spring Data integration related configuration files.
+ */
+package org.apache.ignite.springdata20.repository.config;
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/package-info.java
new file mode 100644
index 0000000..9df5513
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/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. -->
+ * Package contains Apache Ignite Spring Data integration.
+ */
+package org.apache.ignite.springdata20.repository;
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/DeclaredQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/DeclaredQuery.java
new file mode 100644
index 0000000..297d35c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/DeclaredQuery.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed 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.springdata20.repository.query;
+
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.StringUtils;
+
+/**
+ * A wrapper for a String representation of a query offering information about the query.
+ *
+ * @author Jens Schauder
+ */
+interface DeclaredQuery {
+ /**
+ * Creates a {@literal DeclaredQuery} from a query {@literal String}.
+ *
+ * @param qry might be {@literal null} or empty.
+ * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument.
+ */
+ public static DeclaredQuery of(@Nullable String qry) {
+ return StringUtils.isEmpty(qry) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(qry);
+ }
+
+ /**
+ * @return whether the underlying query has at least one named parameter.
+ */
+ public boolean hasNamedParameter();
+
+ /**
+ * Returns the query string.
+ */
+ public String getQueryString();
+
+ /**
+ * Returns the main alias used in the query.
+ *
+ * @return the alias
+ */
+ @Nullable
+ public String getAlias();
+
+ /**
+ * Returns whether the query is using a constructor expression.
+ */
+ public boolean hasConstructorExpression();
+
+ /**
+ * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
+ */
+ public boolean isDefaultProjection();
+
+ /**
+ * Returns the {@link StringQuery.ParameterBinding}s registered.
+ */
+ public List<StringQuery.ParameterBinding> getParameterBindings();
+
+ /**
+ * Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to
+ * be expected from the original query, either derived from the query wrapped by this instance or from the
+ * information passed as arguments.
+ *
+ * @param cntQry an optional query string to be used if present.
+ * @param cntQryProjection an optional return type for the query.
+ * @return a new {@literal DeclaredQuery} instance.
+ */
+ public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection);
+
+ /**
+ * @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
+ */
+ public default boolean usesPaging() {
+ return false;
+ }
+
+ /**
+ * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
+ * name.
+ *
+ * @return Whether the query uses JDBC style parameters.
+ */
+ public boolean usesJdbcStyleParameters();
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/EmptyDeclaredQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/EmptyDeclaredQuery.java
new file mode 100644
index 0000000..34fdb8c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/EmptyDeclaredQuery.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed 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.springdata20.repository.query;
+
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * NULL-Object pattern implementation for {@link DeclaredQuery}.
+ *
+ * @author Jens Schauder
+ */
+class EmptyDeclaredQuery implements DeclaredQuery {
+ /**
+ * An implementation implementing the NULL-Object pattern for situations where there is no query.
+ */
+ static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery();
+
+ /** {@inheritDoc} */
+ @Override public boolean hasNamedParameter() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getQueryString() {
+ return "";
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getAlias() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean hasConstructorExpression() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean isDefaultProjection() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<StringQuery.ParameterBinding> getParameterBindings() {
+ return Collections.emptyList();
+ }
+
+ /** {@inheritDoc} */
+ @Override public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection) {
+ Assert.hasText(cntQry, "CountQuery must not be empty!");
+ return DeclaredQuery.of(cntQry);
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean usesJdbcStyleParameters() {
+ return false;
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/ExpressionBasedStringQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/ExpressionBasedStringQuery.java
new file mode 100644
index 0000000..b7559a5
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/ExpressionBasedStringQuery.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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.springdata20.repository.query;
+
+import java.util.regex.Pattern;
+import org.springframework.data.repository.core.EntityMetadata;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.util.Assert;
+
+/**
+ * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression.
+ * <p>
+ * Currently the following template variables are available:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the given entity</li>
+ * <ol>
+ *
+ * @author Thomas Darimont
+ * @author Oliver Gierke
+ * @author Tom Hombergs
+ */
+class ExpressionBasedStringQuery extends StringQuery {
+ /**
+ * Expression parameter.
+ */
+ private static final String EXPRESSION_PARAMETER = "?#{";
+
+ /**
+ * Quoted expression parameter.
+ */
+ private static final String QUOTED_EXPRESSION_PARAMETER = "?__HASH__{";
+
+ /**
+ * Expression parameter quoting.
+ */
+ private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile(Pattern.quote(EXPRESSION_PARAMETER));
+
+ /**
+ * Expression parameter unquoting.
+ */
+ private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile(
+ Pattern.quote(QUOTED_EXPRESSION_PARAMETER));
+
+ /**
+ * Entity name.
+ */
+ private static final String ENTITY_NAME = "entityName";
+
+ /**
+ * Entity name variable.
+ */
+ private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME;
+
+ /**
+ * Entity name variable expression.
+ */
+ private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}";
+
+ /**
+ * Creates a new instance for the given query and {@link EntityMetadata}.
+ *
+ * @param qry must not be {@literal null} or empty.
+ * @param metadata must not be {@literal null}.
+ * @param parser must not be {@literal null}.
+ */
+ public ExpressionBasedStringQuery(String qry, RepositoryMetadata metadata, SpelExpressionParser parser) {
+ super(renderQueryIfExpressionOrReturnQuery(qry, metadata, parser));
+ }
+
+ /**
+ * Creates an instance from a given {@link DeclaredQuery}.
+ *
+ * @param qry the original query. Must not be {@literal null}.
+ * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+ * @param parser Parser for resolving SpEL expressions. Must not be {@literal null}.
+ * @return A query supporting SpEL expressions.
+ */
+ static ExpressionBasedStringQuery from(DeclaredQuery qry,
+ RepositoryMetadata metadata,
+ SpelExpressionParser parser) {
+ return new ExpressionBasedStringQuery(qry.getQueryString(), metadata, parser);
+ }
+
+ /**
+ * @param qry, the query expression potentially containing a SpEL expression. Must not be {@literal null}.}
+ * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+ * @param parser Must not be {@literal null}.
+ * @return rendered query
+ */
+ private static String renderQueryIfExpressionOrReturnQuery(String qry,
+ RepositoryMetadata metadata,
+ SpelExpressionParser parser) {
+
+ Assert.notNull(qry, "query must not be null!");
+ Assert.notNull(metadata, "metadata must not be null!");
+ Assert.notNull(parser, "parser must not be null!");
+
+ if (!containsExpression(qry))
+ return qry;
+
+ StandardEvaluationContext evalCtx = new StandardEvaluationContext();
+ evalCtx.setVariable(ENTITY_NAME, metadata.getDomainType().getSimpleName());
+
+ qry = potentiallyQuoteExpressionsParameter(qry);
+
+ Expression expr = parser.parseExpression(qry, ParserContext.TEMPLATE_EXPRESSION);
+
+ String result = expr.getValue(evalCtx, String.class);
+
+ if (result == null)
+ return qry;
+
+ return potentiallyUnquoteParameterExpressions(result);
+ }
+
+ /**
+ * @param result Result.
+ */
+ private static String potentiallyUnquoteParameterExpressions(String result) {
+ return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER);
+ }
+
+ /**
+ * @param qry Query.
+ */
+ private static String potentiallyQuoteExpressionsParameter(String qry) {
+ return EXPRESSION_PARAMETER_QUOTING.matcher(qry).replaceAll(QUOTED_EXPRESSION_PARAMETER);
+ }
+
+ /**
+ * @param qry Query.
+ */
+ private static boolean containsExpression(String qry) {
+ return qry.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQuery.java
new file mode 100644
index 0000000..f8e6d48
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQuery.java
@@ -0,0 +1,134 @@
+/*
+ * 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.springdata20.repository.query;
+
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Ignite query helper class. For internal use only.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteQuery {
+ /** */
+ enum Option {
+ /** Query will be used with Sort object. */
+ SORTING,
+
+ /** Query will be used with Pageable object. */
+ PAGINATION,
+
+ /** No advanced option. */
+ NONE
+ }
+
+ /**
+ * Query text string.
+ */
+ private final String qrySql;
+
+ /**
+ * Whether this is a SQL fields query
+ */
+ private final boolean isFieldQuery;
+
+ /**
+ * Whether this is Text query
+ */
+ private final boolean isTextQuery;
+
+ /**
+ * Whether was autogenerated (by method name)
+ */
+ private final boolean isAutogenerated;
+
+ /**
+ * Type of option.
+ */
+ private final Option option;
+
+ /**
+ * @param qrySql the query string.
+ * @param isFieldQuery Is field query.
+ * @param isTextQuery Is a TextQuery
+ * @param isAutogenerated query was autogenerated
+ * @param option Option.
+ */
+ public IgniteQuery(String qrySql,
+ boolean isFieldQuery,
+ boolean isTextQuery,
+ boolean isAutogenerated,
+ Option option) {
+ this.qrySql = qrySql;
+ this.isFieldQuery = isFieldQuery;
+ this.isTextQuery = isTextQuery;
+ this.isAutogenerated = isAutogenerated;
+ this.option = option;
+ }
+
+ /**
+ * Text string of the query.
+ *
+ * @return SQL query text string.
+ */
+ public String qryStr() {
+ return qrySql;
+ }
+
+ /**
+ * Returns {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+ *
+ * @return {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+ */
+ public boolean isFieldQuery() {
+ return isFieldQuery;
+ }
+
+ /**
+ * Returns {@code true} if it's Ignite Text query, {@code false} otherwise.
+ *
+ * @return {@code true} if it's Ignite Text query, {@code false} otherwise.
+ */
+ public boolean isTextQuery() {
+ return isTextQuery;
+ }
+
+ /**
+ * Returns {@code true} if it's autogenerated, {@code false} otherwise.
+ *
+ * @return {@code true} if it's autogenerated, {@code false} otherwise.
+ */
+ public boolean isAutogenerated() {
+ return isAutogenerated;
+ }
+
+ /**
+ * Advanced querying option.
+ *
+ * @return querying option.
+ */
+ public Option options() {
+ return option;
+ }
+
+ /** */
+ @Override public String toString() {
+ return S.toString(IgniteQuery.class, this);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQueryGenerator.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQueryGenerator.java
new file mode 100644
index 0000000..864903a
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQueryGenerator.java
@@ -0,0 +1,276 @@
+/*
+ * 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.springdata20.repository.query;
+
+import java.lang.reflect.Method;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * Ignite query generator for Spring Data framework.
+ */
+public class IgniteQueryGenerator {
+ /** */
+ private IgniteQueryGenerator() {
+ // No-op.
+ }
+
+ /**
+ * @param mtd Method.
+ * @param metadata Metadata.
+ * @return Generated ignite query.
+ */
+ public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
+ PartTree parts;
+
+ try {
+ parts = new PartTree(mtd.getName(), metadata.getDomainType());
+ }
+ catch (PropertyReferenceException e) {
+ parts = new PartTree(mtd.getName(), metadata.getIdType());
+ }
+
+ boolean isCountOrFieldQuery = parts.isCountProjection();
+
+ StringBuilder sql = new StringBuilder();
+
+ if (parts.isDelete()) {
+ sql.append("DELETE ");
+
+ // For the DML queries aside from SELECT *, they should run over SqlFieldQuery
+ isCountOrFieldQuery = true;
+ }
+ else {
+ sql.append("SELECT ");
+
+ if (parts.isDistinct())
+ throw new UnsupportedOperationException("DISTINCT clause in not supported.");
+
+ if (isCountOrFieldQuery)
+ sql.append("COUNT(1) ");
+ else
+ sql.append("* ");
+ }
+
+ sql.append("FROM ").append(metadata.getDomainType().getSimpleName());
+
+ if (parts.iterator().hasNext()) {
+ sql.append(" WHERE ");
+
+ for (PartTree.OrPart orPart : parts) {
+ sql.append("(");
+
+ for (Part part : orPart) {
+ handleQueryPart(sql, part, metadata.getDomainType());
+ sql.append(" AND ");
+ }
+
+ sql.delete(sql.length() - 5, sql.length());
+
+ sql.append(") OR ");
+ }
+
+ sql.delete(sql.length() - 4, sql.length());
+ }
+
+ addSorting(sql, parts.getSort());
+
+ if (parts.isLimiting()) {
+ sql.append(" LIMIT ");
+ sql.append(parts.getMaxResults().intValue());
+ }
+
+ return new IgniteQuery(sql.toString(), isCountOrFieldQuery, false, true, getOptions(mtd));
+ }
+
+ /**
+ * Add a dynamic part of query for the sorting support.
+ *
+ * @param sql SQL text string.
+ * @param sort Sort method.
+ * @return Sorting criteria in StringBuilder.
+ */
+ public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
+ if (sort != null && sort != Sort.unsorted()) {
+ sql.append(" ORDER BY ");
+
+ for (Sort.Order order : sort) {
+ sql.append(order.getProperty()).append(" ").append(order.getDirection());
+
+ if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
+ sql.append(" ").append("NULL ");
+
+ switch (order.getNullHandling()) {
+ case NULLS_FIRST:
+ sql.append("FIRST");
+ break;
+ case NULLS_LAST:
+ sql.append("LAST");
+ break;
+ default:
+ }
+ }
+ sql.append(", ");
+ }
+
+ sql.delete(sql.length() - 2, sql.length());
+ }
+
+ return sql;
+ }
+
+ /**
+ * Add a dynamic part of a query for the pagination support.
+ *
+ * @param sql Builder instance.
+ * @param pageable Pageable instance.
+ * @return Builder instance.
+ */
+ public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {
+
+ addSorting(sql, pageable.getSort());
+
+ sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());
+
+ return sql;
+ }
+
+ /**
+ * Determines whether query is dynamic or not (by list of method parameters)
+ *
+ * @param mtd Method.
+ * @return type of options
+ */
+ public static IgniteQuery.Option getOptions(Method mtd) {
+ IgniteQuery.Option option = IgniteQuery.Option.NONE;
+
+ Class<?>[] types = mtd.getParameterTypes();
+ if (types.length > 0) {
+ Class<?> type = types[types.length - 1];
+
+ if (Sort.class.isAssignableFrom(type))
+ option = IgniteQuery.Option.SORTING;
+ else if (Pageable.class.isAssignableFrom(type))
+ option = IgniteQuery.Option.PAGINATION;
+ }
+
+ for (int i = 0; i < types.length - 1; i++) {
+ Class<?> tp = types[i];
+
+ if (tp == Sort.class || tp == Pageable.class)
+ throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
+ }
+
+ return option;
+ }
+
+ /**
+ * Check and correct table name if using column name from compound key.
+ */
+ private static String getColumnName(Part part, Class<?> domainType) {
+ PropertyPath prperty = part.getProperty();
+
+ if (prperty.getType() != domainType)
+ return domainType.getSimpleName() + "." + prperty.getSegment();
+ else
+ return part.toString();
+ }
+
+ /**
+ * Transform part to qryStr expression
+ */
+ private static void handleQueryPart(StringBuilder sql, Part part, Class<?> domainType) {
+ sql.append("(");
+
+ sql.append(getColumnName(part, domainType));
+
+ switch (part.getType()) {
+ case SIMPLE_PROPERTY:
+ sql.append("=?");
+ break;
+ case NEGATING_SIMPLE_PROPERTY:
+ sql.append("<>?");
+ break;
+ case GREATER_THAN:
+ sql.append(">?");
+ break;
+ case GREATER_THAN_EQUAL:
+ sql.append(">=?");
+ break;
+ case LESS_THAN:
+ sql.append("<?");
+ break;
+ case LESS_THAN_EQUAL:
+ sql.append("<=?");
+ break;
+ case IS_NOT_NULL:
+ sql.append(" IS NOT NULL");
+ break;
+ case IS_NULL:
+ sql.append(" IS NULL");
+ break;
+ case BETWEEN:
+ sql.append(" BETWEEN ? AND ?");
+ break;
+ case FALSE:
+ sql.append(" = FALSE");
+ break;
+ case TRUE:
+ sql.append(" = TRUE");
+ break;
+ //TODO: review this legacy code, LIKE should be -> LIKE ?
+ case LIKE:
+ case CONTAINING:
+ sql.append(" LIKE '%' || ? || '%'");
+ break;
+ case NOT_CONTAINING:
+ //TODO: review this legacy code, NOT_LIKE should be -> NOT LIKE ?
+ case NOT_LIKE:
+ sql.append(" NOT LIKE '%' || ? || '%'");
+ break;
+ case STARTING_WITH:
+ sql.append(" LIKE ? || '%'");
+ break;
+ case ENDING_WITH:
+ sql.append(" LIKE '%' || ?");
+ break;
+ case IN:
+ sql.append(" IN ?");
+ break;
+ case NOT_IN:
+ sql.append(" NOT IN ?");
+ break;
+ case REGEX:
+ sql.append(" REGEXP ?");
+ break;
+ case NEAR:
+ case AFTER:
+ case BEFORE:
+ case EXISTS:
+ default:
+ throw new UnsupportedOperationException(part.getType() + " is not supported!");
+ }
+
+ sql.append(")");
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteRepositoryQuery.java
new file mode 100644
index 0000000..96ff57b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteRepositoryQuery.java
@@ -0,0 +1,1043 @@
+/*
+ * 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.springdata20.repository.query;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.AbstractCollection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.cache.Cache;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.cache.query.TextQuery;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryUtils;
+import org.apache.ignite.internal.processors.cache.CacheEntryImpl;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.cache.binary.IgniteBinaryImpl;
+import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.springdata20.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata20.repository.query.StringQuery.ParameterBinding;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.Parameter;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.StringUtils;
+
+import static org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactory.isFieldQuery;
+
+/**
+ * Ignite query implementation.
+ * <p>
+ * <p>
+ * Features:
+ * <ol>
+ * <li> Supports query tuning parameters</li>
+ * <li> Supports projections</li>
+ * <li> Supports Page and Stream responses</li>
+ * <li> Supports SqlFieldsQuery resultset transformation into the domain entity</li>
+ * <li> Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam") annotation</li>
+ * <li> Supports advanced parameter binding and SpEL expressions into SQL queries
+ * <ol>
+ * <li><b>Template variables</b>:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the domain entity</li>
+ * </ol>
+ * </li>
+ * <li><b>Method parameter expressions</b>: Parameters are exposed for indexed access ([0] is the first query method's
+ * param) or via the name declared using @Param. The actual SpEL expression binding is triggered by '?#'. Example:
+ * ?#{[0]} or ?#{#myParamName}</li>
+ * <li><b>Advanced SpEL expressions</b>: While advanced parameter binding is a very useful feature, the real power of
+ * SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components
+ * through SpEL EvaluationContext extension model.</li>
+ * </ol>
+ * Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where country = ?#{[0]} and city = ?#{[1]}")
+ * List<User> searchUsersByCity({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * Pageable pageable);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = ?")
+ * User searchUserByEmail(String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where lucene = ?#{
+ * luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder.match('city',#city)).build()}")
+ * List<User> searchUsersByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports SpEL expressions into Text queries ({@link TextQuery}). Examples:
+ * <pre>
+ * {@code @Query}(textQuery = true, value = "email: #{#email}")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(textQuery = true, value = "#{#textToSearch}")
+ * List<User> searchUsersByText({@code @Param}("textToSearch") String text, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{[0]}")
+ * List<User> searchUsersByText(String textToSearch, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder
+ * .match('city', #city)).build()}")
+ * List<User> searchUserByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports dynamic query and tuning at runtime by using {@link DynamicQueryConfig} method parameter. Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmailWithQueryTuning({@code @Param}("email") String email, {@code @Param}("ignoredUsedAsQueryTuning") DynamicQueryConfig config);
+ *
+ * {@code @Query}(dynamicQuery = true)
+ * List<User> searchUsersByCityWithDynamicQuery({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * {@code @Param}("ignoredUsedAsDynamicQueryAndTuning") DynamicQueryConfig config, Pageable pageable);
+ *
+ * ...
+ * DynamicQueryConfig onlyTunning = new DynamicQueryConfig().setCollocated(true);
+ * repo.searchUserByEmailWithQueryTuning("user@mail.com", onlyTunning);
+ *
+ * DynamicQueryConfig withDynamicQuery = new DynamicQueryConfig().value("SELECT * from #{#entityName} where country = ?#{[0] and city = ?#{[1]}").setForceFieldsQuery(true).setLazy(true).setCollocated(true);
+ * repo.searchUsersByCityWithDynamicQuery("Spain", "Madrid", withDynamicQuery, new PageRequest(0, 100));
+ *
+ * </pre>
+ * </li>
+ * </ol>
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@SuppressWarnings("unchecked")
+public class IgniteRepositoryQuery implements RepositoryQuery {
+ /** */
+ private static final TreeMap<String, Class<?>> binaryFieldClass = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ /**
+ * Defines the way how to process query result
+ */
+ private enum ReturnStrategy {
+ /** Need to return only one value. */
+ ONE_VALUE,
+
+ /** Need to return one cache entry */
+ CACHE_ENTRY,
+
+ /** Need to return list of cache entries */
+ LIST_OF_CACHE_ENTRIES,
+
+ /** Need to return list of values */
+ LIST_OF_VALUES,
+
+ /** Need to return list of lists */
+ LIST_OF_LISTS,
+
+ /** Need to return slice */
+ SLICE_OF_VALUES,
+
+ /** Slice of cache entries */
+ SLICE_OF_CACHE_ENTRIES,
+
+ /** Slice of lists */
+ SLICE_OF_LISTS,
+
+ /** Need to return Page of values */
+ PAGE_OF_VALUES,
+
+ /** Need to return stream of values */
+ STREAM_OF_VALUES,
+ }
+
+ /** */
+ private final Class<?> type;
+
+ /** */
+ private final IgniteQuery staticQuery;
+
+ /** */
+ private final IgniteCache cache;
+
+ /** */
+ private final Ignite ignite;
+
+ /** Required by qryStr field query type for binary manipulation */
+ private IgniteBinaryImpl igniteBinary;
+
+ /** */
+ private BinaryType igniteBinType;
+
+ /** */
+ private final Method mtd;
+
+ /** */
+ private final RepositoryMetadata metadata;
+
+ /** */
+ private final ProjectionFactory factory;
+
+ /** */
+ private final ReturnStrategy staticReturnStgy;
+
+ /** Detect if returned data from method is projected */
+ private final boolean hasProjection;
+
+ /** Whether projection is dynamic (provided as method parameter) */
+ private final boolean hasDynamicProjection;
+
+ /** Dynamic projection parameter index */
+ private final int dynamicProjectionIndex;
+
+ /** Dynamic query configuration */
+ private final int dynamicQueryConfigurationIndex;
+
+ /** The return query method */
+ private final QueryMethod qMethod;
+
+ /** The return domain class of QueryMethod */
+ private final Class<?> returnedDomainClass;
+
+ /** */
+ private final SpelExpressionParser expressionParser;
+
+ /** Could provide ExtensionAwareQueryMethodEvaluationContextProvider */
+ private final EvaluationContextProvider queryMethodEvaluationContextProvider;
+
+ /** Static query configuration. */
+ private final DynamicQueryConfig staticQueryConfiguration;
+
+ /**
+ * Instantiates a new Ignite repository query.
+ *
+ * @param ignite the ignite
+ * @param metadata Metadata.
+ * @param staticQuery Query.
+ * @param mtd Method.
+ * @param factory Factory.
+ * @param cache Cache.
+ * @param staticQueryConfiguration the query configuration
+ * @param queryMethodEvaluationContextProvider the query method evaluation context provider
+ */
+ public IgniteRepositoryQuery(Ignite ignite,
+ RepositoryMetadata metadata,
+ @Nullable IgniteQuery staticQuery,
+ Method mtd,
+ ProjectionFactory factory,
+ IgniteCache cache,
+ @Nullable DynamicQueryConfig staticQueryConfiguration,
+ EvaluationContextProvider queryMethodEvaluationContextProvider) {
+ this.metadata = metadata;
+ this.mtd = mtd;
+ this.factory = factory;
+ type = metadata.getDomainType();
+
+ this.cache = cache;
+ this.ignite = ignite;
+
+ this.staticQueryConfiguration = staticQueryConfiguration;
+ this.staticQuery = staticQuery;
+
+ if (this.staticQuery != null)
+ staticReturnStgy = calcReturnType(mtd, this.staticQuery.isFieldQuery());
+ else
+ staticReturnStgy = null;
+
+ expressionParser = new SpelExpressionParser();
+ this.queryMethodEvaluationContextProvider = queryMethodEvaluationContextProvider;
+
+ qMethod = getQueryMethod();
+
+ // control projection
+ hasDynamicProjection = getQueryMethod().getParameters().hasDynamicProjection();
+ hasProjection = hasDynamicProjection || getQueryMethod().getResultProcessor().getReturnedType()
+ .isProjecting();
+
+ dynamicProjectionIndex = qMethod.getParameters().getDynamicProjectionIndex();
+
+ returnedDomainClass = getQueryMethod().getReturnedObjectType();
+
+ dynamicQueryConfigurationIndex = getDynamicQueryConfigurationIndex(qMethod);
+
+ // ensure dynamic query configuration param exists if dynamicQuery = true
+ if (dynamicQueryConfigurationIndex == -1 && this.staticQuery == null) {
+ throw new IllegalStateException(
+ "When passing dynamicQuery = true via org.apache.ignite.springdata.repository.config.Query "
+ + "annotation, you must provide a non null method parameter of type DynamicQueryConfig");
+ }
+ // ensure domain class is registered on marshaller to transform row to entity
+ registerClassOnMarshaller(((IgniteEx)ignite).context(), type);
+ }
+
+ /**
+ * {@inheritDoc} @param values the values
+ *
+ * @return the object
+ */
+ @Override public Object execute(Object[] values) {
+ Object[] parameters = values;
+
+ // config via Query annotation (dynamicQuery = false)
+ DynamicQueryConfig config = staticQueryConfiguration;
+
+ // or condition to allow query tunning
+ if (config == null || dynamicQueryConfigurationIndex != -1) {
+ DynamicQueryConfig newConfig = (DynamicQueryConfig)values[dynamicQueryConfigurationIndex];
+ parameters = ArrayUtils.removeElement(parameters, dynamicQueryConfigurationIndex);
+ if (newConfig != null) {
+ // upset query configuration
+ config = newConfig;
+ }
+ }
+ // query configuration is required, via Query annotation or per parameter (within provided values param)
+ if (config == null) {
+ throw new IllegalStateException(
+ "Unable to execute query. When passing dynamicQuery = true via org.apache.ignite.springdata"
+ + ".repository.config.Query annotation, you must provide a non null method parameter of type "
+ + "DynamicQueryConfig");
+ }
+
+ IgniteQuery qry = getQuery(config);
+
+ ReturnStrategy returnStgy = getReturnStgy(qry);
+
+ Query iQry = prepareQuery(qry, config, returnStgy, parameters);
+
+ QueryCursor qryCursor = cache.query(iQry);
+
+ return transformQueryCursor(qry, returnStgy, parameters, qryCursor);
+ }
+
+ /** {@inheritDoc} */
+ @Override public QueryMethod getQueryMethod() {
+ return new QueryMethod(mtd, metadata, factory);
+ }
+
+ private <T extends Parameter> int getDynamicQueryConfigurationIndex(QueryMethod method) {
+ Iterator<T> it = (Iterator<T>)method.getParameters().iterator();
+ int i = 0;
+ boolean found = false;
+ int index = -1;
+ while (it.hasNext()) {
+ T parameter = it.next();
+
+ if (DynamicQueryConfig.class.isAssignableFrom(parameter.getType())) {
+ if (found) {
+ throw new IllegalStateException("Invalid '" + method.getName() + "' repository method signature. "
+ + "Only ONE DynamicQueryConfig parameter is allowed");
+ }
+
+ found = true;
+ index = i;
+ }
+
+ i++;
+ }
+ return index;
+ }
+
+ /** */
+ private synchronized IgniteBinaryImpl binary() {
+ if (igniteBinary == null)
+ igniteBinary = (IgniteBinaryImpl)ignite.binary();
+
+ return igniteBinary;
+ }
+
+ /** */
+ private synchronized BinaryType binType() {
+ if (igniteBinType == null)
+ igniteBinType = binary().type(type);
+
+ return igniteBinType;
+ }
+
+ /**
+ * @param mtd Method.
+ * @param isFieldQry Is field query.
+ * @return Return strategy type.
+ */
+ private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) {
+ Class<?> returnType = mtd.getReturnType();
+
+ if (returnType == Slice.class) {
+ if (isFieldQry) {
+ if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+ return ReturnStrategy.SLICE_OF_LISTS;
+ }
+ else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+ return ReturnStrategy.SLICE_OF_CACHE_ENTRIES;
+ return ReturnStrategy.SLICE_OF_VALUES;
+ }
+ else if (returnType == Page.class)
+ return ReturnStrategy.PAGE_OF_VALUES;
+ else if (returnType == Stream.class)
+ return ReturnStrategy.STREAM_OF_VALUES;
+ else if (Cache.Entry.class.isAssignableFrom(returnType))
+ return ReturnStrategy.CACHE_ENTRY;
+ else if (Iterable.class.isAssignableFrom(returnType)) {
+ if (isFieldQry) {
+ if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+ return ReturnStrategy.LIST_OF_LISTS;
+ }
+ else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+ return ReturnStrategy.LIST_OF_CACHE_ENTRIES;
+ return ReturnStrategy.LIST_OF_VALUES;
+ }
+ else
+ return ReturnStrategy.ONE_VALUE;
+ }
+
+ /**
+ * @param cls Class.
+ * @param mtd Method.
+ * @return if {@code mtd} return type is assignable from {@code cls}
+ */
+ private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
+ Type genericReturnType = mtd.getGenericReturnType();
+
+ if (!(genericReturnType instanceof ParameterizedType))
+ return false;
+
+ Type[] actualTypeArguments = ((ParameterizedType)genericReturnType).getActualTypeArguments();
+
+ if (actualTypeArguments.length == 0)
+ return false;
+
+ if (actualTypeArguments[0] instanceof ParameterizedType) {
+ ParameterizedType type = (ParameterizedType)actualTypeArguments[0];
+
+ Class<?> type1 = (Class)type.getRawType();
+
+ return type1.isAssignableFrom(cls);
+ }
+
+ if (actualTypeArguments[0] instanceof Class) {
+ Class typeArg = (Class)actualTypeArguments[0];
+
+ return typeArg.isAssignableFrom(cls);
+ }
+
+ return false;
+ }
+
+ /**
+ * When select fields by query H2 returns Timestamp for types java.util.Date and java.qryStr.Timestamp
+ *
+ * @see org.apache.ignite.internal.processors.query.h2.H2DatabaseType map.put(Timestamp.class, TIMESTAMP)
+ * map.put(java.util.Date.class, TIMESTAMP) map.put(java.qryStr.Date.class, DATE)
+ */
+ private static <T> T fixExpectedType(final Object object, final Class<T> expected) {
+ if (expected != null && object instanceof java.sql.Timestamp && expected.equals(java.util.Date.class))
+ return (T)new java.util.Date(((java.sql.Timestamp)object).getTime());
+
+ return (T)object;
+ }
+
+ /**
+ * @param cfg Config.
+ */
+ private IgniteQuery getQuery(@Nullable DynamicQueryConfig cfg) {
+ if (staticQuery != null)
+ return staticQuery;
+
+ if (cfg != null && (StringUtils.hasText(cfg.value()) || cfg.textQuery())) {
+ return new IgniteQuery(cfg.value(),
+ !cfg.textQuery() && (isFieldQuery(cfg.value()) || cfg.forceFieldsQuery()), cfg.textQuery(),
+ false, IgniteQueryGenerator.getOptions(mtd));
+ }
+
+ throw new IllegalStateException("Unable to obtain a valid query. When passing dynamicQuery = true via org"
+ + ".apache.ignite.springdata.repository.config.Query annotation, you must"
+ + " provide a non null method parameter of type DynamicQueryConfig with a "
+ + "non empty value (query string) or textQuery = true");
+ }
+
+ /**
+ * @param qry Query.
+ */
+ private ReturnStrategy getReturnStgy(IgniteQuery qry) {
+ if (staticReturnStgy != null)
+ return staticReturnStgy;
+
+ if (qry != null)
+ return calcReturnType(mtd, qry.isFieldQuery());
+
+ throw new IllegalStateException("Unable to obtain a valid return strategy. When passing dynamicQuery = true "
+ + "via org.apache.ignite.springdata.repository.config.Query annotation, "
+ + "you must provide a non null method parameter of type "
+ + "DynamicQueryConfig with a non empty value (query string) or textQuery "
+ + "= true");
+ }
+
+ /**
+ * @param cls Class.
+ */
+ private static boolean isPrimitiveOrWrapper(Class<?> cls) {
+ return cls.isPrimitive() ||
+ Boolean.class.equals(cls) ||
+ Byte.class.equals(cls) ||
+ Character.class.equals(cls) ||
+ Short.class.equals(cls) ||
+ Integer.class.equals(cls) ||
+ Long.class.equals(cls) ||
+ Float.class.equals(cls) ||
+ Double.class.equals(cls) ||
+ Void.class.equals(cls) ||
+ String.class.equals(cls) ||
+ UUID.class.equals(cls);
+ }
+
+ /**
+ * @param prmtrs Prmtrs.
+ * @param qryCursor Query cursor.
+ * @return Query cursor or slice
+ */
+ @Nullable
+ private Object transformQueryCursor(IgniteQuery qry,
+ ReturnStrategy returnStgy,
+ Object[] prmtrs,
+ QueryCursor qryCursor) {
+ final Class<?> returnClass;
+
+ if (hasProjection) {
+ if (hasDynamicProjection)
+ returnClass = (Class<?>)prmtrs[dynamicProjectionIndex];
+ else
+ returnClass = returnedDomainClass;
+ }
+ else
+ returnClass = returnedDomainClass;
+
+ if (qry.isFieldQuery()) {
+ // take control over single primite result from queries, i.e. DELETE, SELECT COUNT, UPDATE ...
+ boolean singlePrimitiveResult = isPrimitiveOrWrapper(returnClass);
+
+ final List<GridQueryFieldMetadata> meta = ((QueryCursorEx)qryCursor).fieldsMeta();
+
+ Function<List<?>, ?> cWrapperTransformFunction = null;
+
+ if (type.equals(returnClass)) {
+ IgniteBinaryImpl binary = binary();
+ BinaryType binType = binType();
+ cWrapperTransformFunction = row -> rowToEntity(binary, binType, row, meta);
+ }
+ else {
+ if (hasProjection || singlePrimitiveResult) {
+ if (singlePrimitiveResult)
+ cWrapperTransformFunction = row -> row.get(0);
+ else {
+ // Map row -> projection class
+ cWrapperTransformFunction = row -> factory
+ .createProjection(returnClass, rowToMap(row, meta));
+ }
+ }
+ else
+ cWrapperTransformFunction = row -> rowToMap(row, meta);
+ }
+
+ QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<List<?>>)qryCursor,
+ cWrapperTransformFunction);
+
+ switch (returnStgy) {
+ case PAGE_OF_VALUES:
+ return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+ case LIST_OF_VALUES:
+ return cWrapper.getAll();
+ case STREAM_OF_VALUES:
+ return cWrapper.stream();
+ case ONE_VALUE:
+ Iterator<?> iter = cWrapper.iterator();
+ if (iter.hasNext()) {
+ Object resp = iter.next();
+ U.closeQuiet(cWrapper);
+ return resp;
+ }
+ return null;
+ case SLICE_OF_VALUES:
+ return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case SLICE_OF_LISTS:
+ return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case LIST_OF_LISTS:
+ return qryCursor.getAll();
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ else {
+ Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor;
+
+ Function<CacheEntryImpl, ?> cWrapperTransformFunction;
+
+ if (hasProjection && !type.equals(returnClass))
+ cWrapperTransformFunction = row -> factory.createProjection(returnClass, row.getValue());
+ else
+ cWrapperTransformFunction = row -> row.getValue();
+
+ QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<CacheEntryImpl>)qryCursor,
+ cWrapperTransformFunction);
+
+ switch (returnStgy) {
+ case PAGE_OF_VALUES:
+ return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+ case LIST_OF_VALUES:
+ return cWrapper.getAll();
+ case STREAM_OF_VALUES:
+ return cWrapper.stream();
+ case ONE_VALUE:
+ Iterator<?> iter1 = cWrapper.iterator();
+ if (iter1.hasNext()) {
+ Object resp = iter1.next();
+ U.closeQuiet(cWrapper);
+ return resp;
+ }
+ return null;
+ case CACHE_ENTRY:
+ Iterator<?> iter2 = qryIter.iterator();
+ if (iter2.hasNext()) {
+ Object resp2 = iter2.next();
+ U.closeQuiet(qryCursor);
+ return resp2;
+ }
+ return null;
+ case SLICE_OF_VALUES:
+ return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case SLICE_OF_CACHE_ENTRIES:
+ return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case LIST_OF_CACHE_ENTRIES:
+ return qryCursor.getAll();
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * Extract bindable values
+ *
+ * @param values values invoking query method
+ * @param queryMethodParams query method parameter definitions
+ * @param queryBindings All parameters found on query string that need to be binded
+ * @return new list of parameters
+ */
+ private Object[] extractBindableValues(Object[] values,
+ Parameters<?, ?> queryMethodParams,
+ List<ParameterBinding> queryBindings) {
+ // no binding params then exit
+ if (queryBindings.isEmpty())
+ return values;
+
+ Object[] newValues = new Object[queryBindings.size()];
+
+ // map bindable parameters from query method: (index/name) - index
+ HashMap<String, Integer> methodParams = new HashMap<>();
+
+ // create an evaluation context for custom query
+ EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+ .getEvaluationContext(queryMethodParams, values);
+
+ // By default queryEvalContext:
+ // - make accesible query method parameters by index:
+ // @Query("select u from User u where u.age = ?#{[0]}")
+ // List<User> findUsersByAge(int age);
+ // - make accesible query method parameters by name:
+ // @Query("select u from User u where u.firstname = ?#{#customer.firstname}")
+ // List<User> findUsersByCustomersFirstname(@Param("customer") Customer customer);
+
+ // query method param's index by name and position
+ queryMethodParams.getBindableParameters().forEach(p -> {
+ if (p.isNamedParameter()) {
+ // map by name (annotated by @Param)
+ methodParams.put(p.getName().get(), p.getIndex());
+ }
+ // map by position
+ methodParams.put(String.valueOf(p.getIndex()), p.getIndex());
+ });
+
+ // process all parameters on query and extract new values to bind
+ for (int i = 0; i < queryBindings.size(); i++) {
+ ParameterBinding p = queryBindings.get(i);
+
+ if (p.isExpression()) {
+ // Evaluate SpEl expressions (synthetic parameter value) , example ?#{#customer.firstname}
+ newValues[i] = expressionParser.parseExpression(p.getExpression()).getValue(queryEvalContext);
+ }
+ else {
+ // Extract parameter value by name or position respectively from invoking values
+ newValues[i] = values[methodParams.get(
+ p.getName() != null ? p.getName() : String.valueOf(p.getRequiredPosition() - 1))];
+ }
+ }
+
+ return newValues;
+ }
+
+ /**
+ * @param qry Query.
+ * @param config Config.
+ * @param returnStgy Return stgy.
+ * @param values Values.
+ * @return prepared query for execution
+ */
+ private Query prepareQuery(IgniteQuery qry, DynamicQueryConfig config, ReturnStrategy returnStgy, Object[] values) {
+ Object[] parameters = values;
+
+ String queryString = qry.qryStr();
+
+ Query query;
+
+ checkRequiredPageable(returnStgy, values);
+
+ if (!qry.isTextQuery()) {
+ if (!qry.isAutogenerated()) {
+ StringQuery squery = new ExpressionBasedStringQuery(queryString, metadata, expressionParser);
+ queryString = squery.getQueryString();
+ parameters = extractBindableValues(parameters, getQueryMethod().getParameters(),
+ squery.getParameterBindings());
+ }
+ else {
+ // remove dynamic projection from parameters
+ if (hasDynamicProjection)
+ parameters = ArrayUtils.remove(parameters, dynamicProjectionIndex);
+ }
+
+ switch (qry.options()) {
+ case SORTING:
+ queryString = IgniteQueryGenerator
+ .addSorting(new StringBuilder(queryString), (Sort)values[values.length - 1])
+ .toString();
+ if (qry.isAutogenerated())
+ parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+ break;
+ case PAGINATION:
+ queryString = IgniteQueryGenerator
+ .addPaging(new StringBuilder(queryString), (Pageable)values[values.length - 1])
+ .toString();
+ if (qry.isAutogenerated())
+ parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+ break;
+ default:
+ }
+
+ if (qry.isFieldQuery()) {
+ SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(queryString);
+ sqlFieldsQry.setArgs(parameters);
+
+ sqlFieldsQry.setCollocated(config.collocated());
+ sqlFieldsQry.setDistributedJoins(config.distributedJoins());
+ sqlFieldsQry.setEnforceJoinOrder(config.enforceJoinOrder());
+ sqlFieldsQry.setLazy(config.lazy());
+ sqlFieldsQry.setLocal(config.local());
+
+ if (config.parts() != null && config.parts().length > 0)
+ sqlFieldsQry.setPartitions(config.parts());
+
+ sqlFieldsQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+ query = sqlFieldsQry;
+ }
+ else {
+ SqlQuery sqlQry = new SqlQuery(type, queryString);
+ sqlQry.setArgs(parameters);
+
+ sqlQry.setDistributedJoins(config.distributedJoins());
+ sqlQry.setLocal(config.local());
+
+ if (config.parts() != null && config.parts().length > 0)
+ sqlQry.setPartitions(config.parts());
+
+ sqlQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+ query = sqlQry;
+ }
+ }
+ else {
+ int pageSize = -1;
+
+ switch (qry.options()) {
+ case PAGINATION:
+ pageSize = ((Pageable)parameters[parameters.length - 1]).getPageSize();
+ break;
+ }
+
+ // check if queryString contains SpEL template expressions and evaluate them if any
+ if (queryString.contains("#{")) {
+ EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+ .getEvaluationContext(getQueryMethod().getParameters(),
+ values);
+
+ Object eval = expressionParser.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION)
+ .getValue(queryEvalContext);
+
+ if (!(eval instanceof String)) {
+ throw new IllegalStateException(
+ "TextQuery with SpEL expressions must produce a String response, but found " + eval.getClass()
+ .getName()
+ + ". Please, check your expression: " + queryString);
+ }
+ queryString = (String)eval;
+ }
+
+ TextQuery textQuery = new TextQuery(type, queryString, config.limit());
+
+ textQuery.setLocal(config.local());
+
+ if (pageSize > -1)
+ textQuery.setPageSize(pageSize);
+
+ query = textQuery;
+ }
+ return query;
+ }
+
+ /** */
+ private static Map<String, Object> rowToMap(final List<?> row, final List<GridQueryFieldMetadata> meta) {
+ // use treemap with case insensitive property name
+ final TreeMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0; i < meta.size(); i++) {
+ // don't want key or val columns
+ final String metaField = meta.get(i).fieldName().toLowerCase();
+ if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+ QueryUtils.VAL_FIELD_NAME))
+ map.put(metaField, row.get(i));
+ }
+ return map;
+ }
+
+ /**
+ * convert row ( with list of field values) into domain entity
+ */
+ private <V> V rowToEntity(final IgniteBinaryImpl binary,
+ final BinaryType binType,
+ final List<?> row,
+ final List<GridQueryFieldMetadata> meta) {
+ // additional data returned by query not present on domain object type
+ final TreeMap<String, Object> metadata = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ final BinaryObjectBuilder bldr = binary.builder(binType.typeName());
+
+ for (int i = 0; i < row.size(); i++) {
+ final GridQueryFieldMetadata fMeta = meta.get(i);
+ final String metaField = fMeta.fieldName();
+ // add existing entity fields to binary object
+ if (binType.field(fMeta.fieldName()) != null && !metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME)
+ && !metaField.equalsIgnoreCase(QueryUtils.VAL_FIELD_NAME)) {
+ final Object fieldValue = row.get(i);
+ if (fieldValue != null) {
+ final Class<?> clazz = getClassForBinaryField(binary, binType, fMeta);
+ // null values must not be set into binary objects
+ bldr.setField(metaField, fixExpectedType(fieldValue, clazz));
+ }
+ }
+ else {
+ // don't want key or val column... but wants null values
+ if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+ QueryUtils.VAL_FIELD_NAME))
+ metadata.put(metaField, row.get(i));
+ }
+ }
+ return bldr.build().deserialize();
+ }
+
+ /**
+ * Obtains real field class from resultset metadata field whether it's available
+ */
+ private Class<?> getClassForBinaryField(final IgniteBinaryImpl binary,
+ final BinaryType binType,
+ final GridQueryFieldMetadata fieldMeta) {
+ try {
+ final String fieldId = fieldMeta.schemaName() + "." + fieldMeta.typeName() + "." + fieldMeta.fieldName();
+
+ if (binaryFieldClass.containsKey(fieldId))
+ return binaryFieldClass.get(fieldId);
+
+ Class<?> clazz = null;
+
+ synchronized (binaryFieldClass) {
+
+ if (binaryFieldClass.containsKey(fieldId))
+ return binaryFieldClass.get(fieldId);
+
+ String fieldName = null;
+
+ // search field name on binary type (query returns case insensitive
+ // field name) but BinaryType is not case insensitive
+ for (final String fname : binType.fieldNames()) {
+ if (fname.equalsIgnoreCase(fieldMeta.fieldName())) {
+ fieldName = fname;
+ break;
+ }
+ }
+
+ final CacheObjectBinaryProcessorImpl proc = (CacheObjectBinaryProcessorImpl)binary.processor();
+
+ // search for class by typeId, if not found use
+ // fieldMeta.fieldTypeName() class
+ clazz = BinaryUtils.resolveClass(proc.binaryContext(), binary.typeId(binType.fieldTypeName(fieldName)),
+ fieldMeta.fieldTypeName(), ignite.configuration().getClassLoader(), true);
+
+ binaryFieldClass.put(fieldId, clazz);
+ }
+
+ return clazz;
+ }
+ catch (final Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Validates operations that requires Pageable parameter
+ *
+ * @param returnStgy Return stgy.
+ * @param prmtrs Prmtrs.
+ */
+ private void checkRequiredPageable(ReturnStrategy returnStgy, Object[] prmtrs) {
+ try {
+ if (returnStgy == ReturnStrategy.PAGE_OF_VALUES || returnStgy == ReturnStrategy.SLICE_OF_VALUES
+ || returnStgy == ReturnStrategy.SLICE_OF_CACHE_ENTRIES) {
+ Pageable page = (Pageable)prmtrs[prmtrs.length - 1];
+ page.isPaged();
+ }
+ }
+ catch (NullPointerException | IndexOutOfBoundsException | ClassCastException e) {
+ throw new IllegalStateException(
+ "For " + returnStgy.name() + " you must provide on last method parameter a non null Pageable instance");
+ }
+ }
+
+ /**
+ * @param ctx Context.
+ * @param clazz Clazz.
+ */
+ private static void registerClassOnMarshaller(final GridKernalContext ctx, final Class<?> clazz) {
+ try {
+ // ensure class registration for marshaller on cluster...
+ if (!U.isJdk(clazz))
+ U.marshal(ctx, clazz.newInstance());
+ }
+ catch (final Exception ignored) {
+ // silent
+ }
+ }
+
+ /**
+ * Ignite QueryCursor wrapper.
+ * <p>
+ * Ensures closing underline cursor when there is no data.
+ *
+ * @param <T> input type
+ * @param <V> transformed output type
+ */
+ public static class QueryCursorWrapper<T, V> extends AbstractCollection<V> implements QueryCursor<V> {
+ /**
+ * Delegate query cursor.
+ */
+ private final QueryCursor<T> delegate;
+
+ /**
+ * Transformer.
+ */
+ private final Function<T, V> transformer;
+
+ /**
+ * Instantiates a new Query cursor wrapper.
+ *
+ * @param delegate delegate QueryCursor with T input elements
+ * @param transformer Function to transform T to V elements
+ */
+ public QueryCursorWrapper(final QueryCursor<T> delegate, final Function<T, V> transformer) {
+ this.delegate = delegate;
+ this.transformer = transformer;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterator<V> iterator() {
+ final Iterator<T> it = delegate.iterator();
+
+ return new Iterator<V>() {
+ /** */
+ @Override public boolean hasNext() {
+ if (!it.hasNext()) {
+ U.closeQuiet(delegate);
+ return false;
+ }
+ return true;
+ }
+
+ /** */
+ @Override public V next() {
+ final V r = transformer.apply(it.next());
+ if (r != null)
+ return r;
+ throw new NoSuchElementException();
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override public void close() {
+ U.closeQuiet(delegate);
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<V> getAll() {
+ final List<V> data = new ArrayList<>();
+ delegate.forEach(i -> data.add(transformer.apply(i)));
+ U.closeQuiet(delegate);
+ return data;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int size() {
+ return 0;
+ }
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/QueryUtils.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/QueryUtils.java
new file mode 100644
index 0000000..c586770
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/QueryUtils.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2008-2019 the original author or authors.
+ *
+ * Licensed 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.springdata20.repository.query;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.util.Streamable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.compile;
+
+/**
+ * Simple utility class to create queries.
+ *
+ * @author Oliver Gierke
+ * @author Kevin Raymond
+ * @author Thomas Darimont
+ * @author Komi Innocent
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Sébastien Péralta
+ * @author Jens Schauder
+ * @author Nils Borrmann
+ * @author Reda.Housni -Alaoui
+ */
+public abstract class QueryUtils {
+ /**
+ * The constant COUNT_QUERY_STRING.
+ */
+ public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
+
+ /**
+ * The constant DELETE_ALL_QUERY_STRING.
+ */
+ public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
+
+ /**
+ * Used Regex/Unicode categories (see http://www.unicode.org/reports/tr18/#General_Category_Property): Z Separator
+ * Cc Control Cf Format P Punctuation
+ */
+ private static final String IDENTIFIER = "[._[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{P}]]+";
+
+ /**
+ * The Colon no double colon.
+ */
+ static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
+
+ /**
+ * The Identifier group.
+ */
+ static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
+
+ /** */
+ private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
+
+ /** */
+ private static final String SIMPLE_COUNT_VALUE = "$2";
+
+ /** */
+ private static final String COMPLEX_COUNT_VALUE = "$3$6";
+
+ /** */
+ private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*$";
+
+ /** */
+ private static final Pattern ALIAS_MATCH;
+
+ /** */
+ private static final Pattern COUNT_MATCH;
+
+ /** */
+ private static final Pattern PROJECTION_CLAUSE = Pattern
+ .compile("select\\s+(.+)\\s+from", Pattern.CASE_INSENSITIVE);
+
+ /** */
+ private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
+
+ /** */
+ private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
+
+ /** */
+ private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
+
+ /** */
+ private static final Pattern NAMED_PARAMETER = Pattern.compile(
+ COLON_NO_DOUBLE_COLON + IDENTIFIER + "|\\#" + IDENTIFIER, CASE_INSENSITIVE);
+
+ /** */
+ private static final Pattern CONSTRUCTOR_EXPRESSION;
+
+ /** */
+ private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
+
+ /** */
+ private static final int VARIABLE_NAME_GROUP_INDEX = 4;
+
+ /** */
+ private static final Pattern FUNCTION_PATTERN;
+
+ static {
+ StringBuilder builder = new StringBuilder();
+ builder.append("(?<=from)"); // from as starting delimiter
+ builder.append("(?:\\s)+"); // at least one space separating
+ builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
+ builder.append("(?:\\sas)*"); // exclude possible "as" keyword
+ builder.append("(?:\\s)+"); // at least one space separating
+ builder.append("(?!(?:where))(\\w+)"); // the actual alias
+
+ ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+ builder = new StringBuilder();
+ builder.append("(select\\s+((distinct )?(.+?)?)\\s+)?(from\\s+");
+ builder.append(IDENTIFIER);
+ builder.append("(?:\\s+as)?\\s+)");
+ builder.append(IDENTIFIER_GROUP);
+ builder.append("(.*)");
+
+ COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+ builder = new StringBuilder();
+ builder.append("select");
+ builder.append("\\s+"); // at least one space separating
+ builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
+ builder.append("new");
+ builder.append("\\s+"); // at least one space separating
+ builder.append(IDENTIFIER);
+ builder.append("\\s*"); // zero to unlimited space separating
+ builder.append("\\(");
+ builder.append(".*");
+ builder.append("\\)");
+
+ CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);
+
+ builder = new StringBuilder();
+ // any function call including parameters within the brackets
+ builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
+ // the potential alias
+ builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
+
+ FUNCTION_PATTERN = compile(builder.toString());
+ }
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private QueryUtils() {
+ // No-op.
+ }
+
+ /**
+ * Returns the query string to execute an exists query for the given id attributes.
+ *
+ * @param entityName the name of the entity to create the query for, must not be {@literal null}.
+ * @param cntQryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
+ * @param idAttrs the id attributes for the entity, must not be {@literal null}.
+ * @return the exists query string
+ */
+ public static String getExistsQueryString(String entityName,
+ String cntQryPlaceHolder,
+ Iterable<String> idAttrs) {
+ String whereClause = Streamable.of(idAttrs).stream() //
+ .map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute,
+ idAttribute)) //
+ .collect(Collectors.joining(" AND ", " WHERE ", ""));
+
+ return String.format(COUNT_QUERY_STRING, cntQryPlaceHolder, entityName) + whereClause;
+ }
+
+ /**
+ * Returns the query string for the given class name.
+ *
+ * @param template must not be {@literal null}.
+ * @param entityName must not be {@literal null}.
+ * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal
+ * null}.
+ */
+ public static String getQueryString(String template, String entityName) {
+ Assert.hasText(entityName, "Entity name must not be null or empty!");
+
+ return String.format(template, entityName);
+ }
+
+ /**
+ * Returns the aliases used for {@code left (outer) join}s.
+ *
+ * @param qry a query string to extract the aliases of joins from. Must not be {@literal null}.
+ * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
+ */
+ static Set<String> getOuterJoinAliases(String qry) {
+ Set<String> result = new HashSet<>();
+ Matcher matcher = JOIN_PATTERN.matcher(qry);
+
+ while (matcher.find()) {
+ String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
+ if (StringUtils.hasText(alias))
+ result.add(alias);
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
+ *
+ * @param qry a {@literal String} containing a query. Must not be {@literal null}.
+ * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
+ */
+ static Set<String> getFunctionAliases(String qry) {
+ Set<String> result = new HashSet<>();
+ Matcher matcher = FUNCTION_PATTERN.matcher(qry);
+
+ while (matcher.find()) {
+ String alias = matcher.group(1);
+
+ if (StringUtils.hasText(alias))
+ result.add(alias);
+ }
+
+ return result;
+ }
+
+ /**
+ * Resolves the alias for the entity to be retrieved from the given JPA query.
+ *
+ * @param qry must not be {@literal null}.
+ * @return Might return {@literal null}.
+ */
+ @Nullable
+ static String detectAlias(String qry) {
+ Matcher matcher = ALIAS_MATCH.matcher(qry);
+
+ return matcher.find() ? matcher.group(2) : null;
+ }
+
+ /**
+ * Creates a count projected query from the given original query.
+ *
+ * @param originalQry must not be {@literal null}.
+ * @param cntProjection may be {@literal null}.
+ * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
+ */
+ static String createCountQueryFor(String originalQry, @Nullable String cntProjection) {
+ Assert.hasText(originalQry, "OriginalQuery must not be null or empty!");
+
+ Matcher matcher = COUNT_MATCH.matcher(originalQry);
+ String countQuery;
+
+ if (cntProjection == null) {
+ String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
+ boolean useVariable = variable != null && StringUtils.hasText(variable) && !variable.startsWith("new")
+ && !variable.startsWith("count(") && !variable.contains(",");
+
+ String replacement = useVariable ? SIMPLE_COUNT_VALUE : COMPLEX_COUNT_VALUE;
+ countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
+ }
+ else
+ countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, cntProjection));
+
+ return countQuery.replaceFirst(ORDER_BY_PART, "");
+ }
+
+ /**
+ * Returns whether the given JPQL query contains a constructor expression.
+ *
+ * @param qry must not be {@literal null} or empty.
+ * @return boolean
+ */
+ public static boolean hasConstructorExpression(String qry) {
+ Assert.hasText(qry, "Query must not be null or empty!");
+
+ return CONSTRUCTOR_EXPRESSION.matcher(qry).find();
+ }
+
+ /**
+ * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
+ *
+ * @param qry must not be {@literal null} or empty.
+ * @return projection
+ */
+ public static String getProjection(String qry) {
+ Assert.hasText(qry, "Query must not be null or empty!");
+
+ Matcher matcher = PROJECTION_CLAUSE.matcher(qry);
+ String projection = matcher.find() ? matcher.group(1) : "";
+ return projection.trim();
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/StringQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/StringQuery.java
new file mode 100644
index 0000000..7280276
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/StringQuery.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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.springdata20.repository.query;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.springdata20.repository.query.spel.SpelQueryContext;
+import org.apache.ignite.springdata20.repository.query.spel.SpelQueryContext.SpelExtractor;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.repository.query.parser.Part.Type;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static org.springframework.util.ObjectUtils.nullSafeEquals;
+import static org.springframework.util.ObjectUtils.nullSafeHashCode;
+
+/**
+ * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned
+ * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations
+ * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
+ * with synthetic bind parameters
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Oliver Wehrens
+ * @author Mark Paluch
+ * @author Jens Schauder
+ */
+class StringQuery implements DeclaredQuery {
+ /** */
+ private final String query;
+
+ /** */
+ private final List<ParameterBinding> bindings;
+
+ /** */
+ @Nullable
+ private final String alias;
+
+ /** */
+ private final boolean hasConstructorExpression;
+
+ /** */
+ private final boolean containsPageableInSpel;
+
+ /** */
+ private final boolean usesJdbcStyleParameters;
+
+ /**
+ * Creates a new {@link StringQuery} from the given JPQL query.
+ *
+ * @param query must not be {@literal null} or empty.
+ */
+ StringQuery(String query) {
+ Assert.hasText(query, "Query must not be null or empty!");
+
+ bindings = new ArrayList<>();
+ containsPageableInSpel = query.contains("#pageable");
+
+ Metadata queryMeta = new Metadata();
+ this.query = ParameterBindingParser.INSTANCE
+ .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, bindings,
+ queryMeta);
+
+ usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
+ alias = QueryUtils.detectAlias(query);
+ hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
+ }
+
+ /**
+ * Returns whether we have found some like bindings.
+ */
+ boolean hasParameterBindings() {
+ return !bindings.isEmpty();
+ }
+
+ /** */
+ String getProjection() {
+ return QueryUtils.getProjection(query);
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#getParameterBindings()
+ /** {@inheritDoc} */
+ @Override public List<ParameterBinding> getParameterBindings() {
+ return bindings;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#deriveCountQuery(java.lang.String, java.lang
+ /** {@inheritDoc} */
+ @Override public DeclaredQuery deriveCountQuery(@Nullable String countQuery,
+ @Nullable String countQueryProjection) {
+ return DeclaredQuery
+ .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesJdbcStyleParameters()
+ /** */
+ @Override public boolean usesJdbcStyleParameters() {
+ return usesJdbcStyleParameters;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#getQueryString()
+ /** {@inheritDoc} */
+ @Override public String getQueryString() {
+ return query;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#getAlias()
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String getAlias() {
+ return alias;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasConstructorExpression()
+ /** {@inheritDoc} */
+ @Override public boolean hasConstructorExpression() {
+ return hasConstructorExpression;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#isDefaultProjection()
+ /** {@inheritDoc} */
+ @Override public boolean isDefaultProjection() {
+ return getProjection().equalsIgnoreCase(alias);
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasNamedParameter()
+ /** {@inheritDoc} */
+ @Override public boolean hasNamedParameter() {
+ return bindings.stream().anyMatch(b -> b.getName() != null);
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesPaging()
+ /** {@inheritDoc} */
+ @Override public boolean usesPaging() {
+ return containsPageableInSpel;
+ }
+
+ /**
+ * A parser that extracts the parameter bindings from a given query string.
+ *
+ * @author Thomas Darimont
+ */
+ enum ParameterBindingParser {
+ /** */
+ INSTANCE;
+
+ /** */
+ private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
+
+ /** */
+ public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
+ // .....................................................................^ not followed by a hash or a letter.
+ // .................................................................^ zero or more digits.
+ // .............................................................^ start with a question mark.
+
+ /** */
+ private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
+
+ /** */
+ private static final Pattern PARAMETER_BINDING_PATTERN;
+
+ /** */
+ private static final String MESSAGE =
+ "Already found parameter binding with same index / parameter name but differing binding type! "
+ + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same "
+ + "binding.";
+
+ /** */
+ private static final int INDEXED_PARAMETER_GROUP = 4;
+
+ /** */
+ private static final int NAMED_PARAMETER_GROUP = 6;
+
+ /** */
+ private static final int COMPARISION_TYPE_GROUP = 1;
+
+ static {
+ List<String> keywords = new ArrayList<>();
+
+ for (ParameterBindingType type : ParameterBindingType.values()) {
+ if (type.getKeyword() != null) {
+ keywords.add(type.getKeyword());
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("(");
+ builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
+ builder.append(")?");
+ builder.append("(?: )?"); // some whitespace
+ builder.append("\\(?"); // optional braces around parameters
+ builder.append("(");
+ builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
+ builder.append("|"); // or
+
+ // named parameter and the parameter name
+ builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
+
+ builder.append(")");
+ builder.append("\\)?"); // optional braces around parameters
+
+ PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
+ }
+
+ /**
+ * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings.
+ * Returns the cleaned up query.
+ */
+ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
+ List<ParameterBinding> bindings,
+ Metadata queryMeta) {
+ int greatestParamIdx = tryFindGreatestParameterIndexIn(query);
+ boolean parametersShouldBeAccessedByIdx = greatestParamIdx != -1;
+
+ /*
+ * Prefer indexed access over named parameters if only SpEL Expression parameters are present.
+ */
+ if (!parametersShouldBeAccessedByIdx && query.contains("?#{")) {
+ parametersShouldBeAccessedByIdx = true;
+ greatestParamIdx = 0;
+ }
+
+ SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIdx,
+ greatestParamIdx);
+
+ String resultingQry = spelExtractor.getQueryString();
+ Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQry);
+ QuotationMap quotedAreas = new QuotationMap(resultingQry);
+
+ int expressionParamIdx = parametersShouldBeAccessedByIdx ? greatestParamIdx : 0;
+
+ boolean usesJpaStyleParameters = false;
+
+ while (matcher.find()) {
+ if (quotedAreas.isQuoted(matcher.start()))
+ continue;
+
+ String paramIdxStr = matcher.group(INDEXED_PARAMETER_GROUP);
+ String paramName = paramIdxStr != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
+ Integer paramIdx = getParameterIndex(paramIdxStr);
+
+ String typeSrc = matcher.group(COMPARISION_TYPE_GROUP);
+ String expression = spelExtractor
+ .getParameter(paramName == null ? paramIdxStr : paramName);
+ String replacement = null;
+
+ Assert.isTrue(paramIdxStr != null || paramName != null,
+ () -> String.format("We need either a name or an index! Offending query string: %s", query));
+
+ expressionParamIdx++;
+ if (paramIdxStr != null && paramIdxStr.isEmpty()) {
+ queryMeta.usesJdbcStyleParameters = true;
+ paramIdx = expressionParamIdx;
+ }
+ else
+ usesJpaStyleParameters = true;
+
+ // named parameters (:param) will be untouched by spelExtractor, so replace them by ? as we don't
+ // know position
+ if (paramName != null)
+ replacement = "?";
+
+ if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
+ throw new IllegalArgumentException(
+ "Mixing of ? (? or :myNamedParam) parameters and other forms like ?1 (SpEL espressions or "
+ + "indexed) is not supported!. Please, if you are using expressions or "
+ + "indexed params, replace all named parameters by expressions. Example :myNamedParam "
+ + "by ?#{#myNamedParam}.");
+ }
+
+ switch (ParameterBindingType.of(typeSrc)) {
+ case LIKE:
+ Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
+ replacement = matcher.group(3);
+
+ if (paramIdx != null)
+ checkAndRegister(new LikeParameterBinding(paramIdx, likeType, expression), bindings);
+ else {
+ checkAndRegister(new LikeParameterBinding(paramName, likeType, expression), bindings);
+
+ replacement = expression != null ? ":" + paramName : matcher.group(5);
+ }
+
+ break;
+
+ case IN:
+ if (paramIdx != null)
+ checkAndRegister(new InParameterBinding(paramIdx, expression), bindings);
+ else
+ checkAndRegister(new InParameterBinding(paramName, expression), bindings);
+
+ break;
+
+ case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
+ default:
+ bindings.add(paramIdx != null
+ ? new ParameterBinding(null, paramIdx, expression)
+ : new ParameterBinding(paramName, null, expression));
+ }
+
+ if (replacement != null)
+ resultingQry = replaceFirst(resultingQry, matcher.group(2), replacement);
+ }
+
+ return resultingQry;
+ }
+
+ /** */
+ private static SpelExtractor createSpelExtractor(String queryWithSpel,
+ boolean parametersShouldBeAccessedByIndex,
+ int greatestParameterIndex) {
+
+ /*
+ * If parameters need to be bound by index, we bind the synthetic expression parameters starting from
+ * position of the greatest discovered index parameter in order to
+ * not mix-up with the actual parameter indices.
+ */
+ int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
+
+ BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
+ ? (index, expression) -> String.valueOf(
+ index + expressionParameterIndex + 1)
+ : (index, expression) ->
+ EXPRESSION_PARAMETER_PREFIX + (index
+ + 1);
+
+ String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
+
+ BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
+
+ return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
+ }
+
+ /** */
+ private static String replaceFirst(String text, String substring, String replacement) {
+ int index = text.indexOf(substring);
+ if (index < 0)
+ return text;
+
+ return text.substring(0, index) + replacement + text.substring(index + substring.length());
+ }
+
+ /** */
+ @Nullable
+ private static Integer getParameterIndex(@Nullable String parameterIndexString) {
+ if (parameterIndexString == null || parameterIndexString.isEmpty())
+ return null;
+ return Integer.valueOf(parameterIndexString);
+ }
+
+ /** */
+ private static int tryFindGreatestParameterIndexIn(String query) {
+ Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
+
+ int greatestParameterIndex = -1;
+ while (parameterIndexMatcher.find()) {
+
+ String parameterIndexString = parameterIndexMatcher.group(1);
+ Integer parameterIndex = getParameterIndex(parameterIndexString);
+ if (parameterIndex != null)
+ greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
+ }
+
+ return greatestParameterIndex;
+ }
+
+ /** */
+ private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
+
+ bindings.stream() //
+ .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) //
+ .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
+
+ if (!bindings.contains(binding))
+ bindings.add(binding);
+ }
+
+ /**
+ * An enum for the different types of bindings.
+ *
+ * @author Thomas Darimont
+ * @author Oliver Gierke
+ */
+ private enum ParameterBindingType {
+ // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
+ // character, while = does not.
+ /** */
+ LIKE("like "),
+
+ /** */
+ IN("in "),
+
+ /** */
+ AS_IS(null);
+
+ /** */
+ @Nullable
+ private final String keyword;
+
+ /** */
+ ParameterBindingType(@Nullable String keyword) {
+ this.keyword = keyword;
+ }
+
+ /**
+ * Returns the keyword that will tirgger the binding type or {@literal null} if the type is not triggered by
+ * a keyword.
+ *
+ * @return the keyword
+ */
+ @Nullable
+ public String getKeyword() {
+ return keyword;
+ }
+
+ /**
+ * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal
+ * #AS_IS} in case no other {@link ParameterBindingType} could be found.
+ */
+ static ParameterBindingType of(String typeSource) {
+ if (!StringUtils.hasText(typeSource))
+ return AS_IS;
+
+ for (ParameterBindingType type : values()) {
+ if (type.name().equalsIgnoreCase(typeSource.trim()))
+ return type;
+ }
+
+ throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
+ }
+ }
+ }
+
+ /**
+ * A generic parameter binding with name or position information.
+ *
+ * @author Thomas Darimont
+ */
+ static class ParameterBinding {
+ /** */
+ @Nullable
+ private final String name;
+
+ /** */
+ @Nullable
+ private final String expression;
+
+ /** */
+ @Nullable
+ private final Integer position;
+
+ /**
+ * Creates a new {@link ParameterBinding} for the parameter with the given position.
+ *
+ * @param position must not be {@literal null}.
+ */
+ ParameterBinding(Integer position) {
+ this(null, position, null);
+ }
+
+ /**
+ * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
+ * information. Either {@literal name} or {@literal position} must be not {@literal null}.
+ *
+ * @param name of the parameter may be {@literal null}.
+ * @param position of the parameter may be {@literal null}.
+ * @param expression the expression to apply to any value for this parameter.
+ */
+ ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) {
+
+ if (name == null)
+ Assert.notNull(position, "Position must not be null!");
+
+ if (position == null)
+ Assert.notNull(name, "Name must not be null!");
+
+ this.name = name;
+ this.position = position;
+ this.expression = expression;
+ }
+
+ /**
+ * Returns whether the binding has the given name. Will always be {@literal false} in case the {@link
+ * ParameterBinding} has been set up from a position.
+ */
+ boolean hasName(@Nullable String name) {
+ return position == null && this.name != null && this.name.equals(name);
+ }
+
+ /**
+ * Returns whether the binding has the given position. Will always be {@literal false} in case the {@link
+ * ParameterBinding} has been set up from a name.
+ */
+ boolean hasPosition(@Nullable Integer position) {
+ return position != null && name == null && position.equals(this.position);
+ }
+
+ /**
+ * @return the name
+ */
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return the name
+ * @throws IllegalStateException if the name is not available.
+ */
+ String getRequiredName() throws IllegalStateException {
+
+ String name = getName();
+
+ if (name != null)
+ return name;
+
+ throw new IllegalStateException(String.format("Required name for %s not available!", this));
+ }
+
+ /**
+ * @return the position
+ */
+ @Nullable
+ Integer getPosition() {
+ return position;
+ }
+
+ /**
+ * @return the position
+ * @throws IllegalStateException if the position is not available.
+ */
+ int getRequiredPosition() throws IllegalStateException {
+
+ Integer position = getPosition();
+
+ if (position != null)
+ return position;
+
+ throw new IllegalStateException(String.format("Required position for %s not available!", this));
+ }
+
+ /**
+ * @return {@literal true} if this parameter binding is a synthetic SpEL expression.
+ */
+ public boolean isExpression() {
+ return expression != null;
+ }
+
+ /** */
+ @Override public int hashCode() {
+
+ int result = 17;
+
+ result += nullSafeHashCode(name);
+ result += nullSafeHashCode(position);
+ result += nullSafeHashCode(expression);
+
+ return result;
+ }
+
+ /** */
+ @Override public boolean equals(Object obj) {
+
+ if (!(obj instanceof ParameterBinding))
+ return false;
+
+ ParameterBinding that = (ParameterBinding)obj;
+
+ return nullSafeEquals(name, that.name) && nullSafeEquals(position, that.position)
+ && nullSafeEquals(expression, that.expression);
+ }
+
+ /** */
+ @Override public String toString() {
+ return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
+ getExpression());
+ }
+
+ /**
+ * @param valueToBind value to prepare
+ */
+ @Nullable
+ public Object prepare(@Nullable Object valueToBind) {
+ return valueToBind;
+ }
+
+ /** */
+ @Nullable
+ public String getExpression() {
+ return expression;
+ }
+ }
+
+ /**
+ * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as
+ * an {@code IN} parameter.
+ *
+ * @author Thomas Darimont
+ */
+ static class InParameterBinding extends ParameterBinding {
+ /**
+ * Creates a new {@link InParameterBinding} for the parameter with the given name.
+ */
+ InParameterBinding(String name, @Nullable String expression) {
+ super(name, null, expression);
+ }
+
+ /**
+ * Creates a new {@link InParameterBinding} for the parameter with the given position.
+ */
+ InParameterBinding(int position, @Nullable String expression) {
+ super(null, position, expression);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object)
+ */
+ @Override public Object prepare(@Nullable Object value) {
+ if (!ObjectUtils.isArray(value))
+ return value;
+
+ int length = Array.getLength(value);
+ Collection<Object> result = new ArrayList<>(length);
+
+ for (int i = 0; i < length; i++)
+ result.add(Array.get(value, i));
+
+ return result;
+ }
+
+ }
+
+ /**
+ * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE
+ * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA.
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ */
+ static class LikeParameterBinding extends ParameterBinding {
+ /** */
+ private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
+ Type.ENDING_WITH, Type.LIKE);
+
+ /** */
+ private final Type type;
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param type must not be {@literal null}.
+ */
+ LikeParameterBinding(String name, Type type) {
+ this(name, type, null);
+ }
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and
+ * parameter binding input.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param type must not be {@literal null}.
+ * @param expression may be {@literal null}.
+ */
+ LikeParameterBinding(String name, Type type, @Nullable String expression) {
+
+ super(name, null, expression);
+
+ Assert.hasText(name, "Name must not be null or empty!");
+ Assert.notNull(type, "Type must not be null!");
+
+ Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+ StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+ this.type = type;
+ }
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+ *
+ * @param position position of the parameter in the query.
+ * @param type must not be {@literal null}.
+ */
+ LikeParameterBinding(int position, Type type) {
+ this(position, type, null);
+ }
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+ *
+ * @param position position of the parameter in the query.
+ * @param type must not be {@literal null}.
+ * @param expression may be {@literal null}.
+ */
+ LikeParameterBinding(int position, Type type, @Nullable String expression) {
+
+ super(null, position, expression);
+
+ Assert.isTrue(position > 0, "Position must be greater than zero!");
+ Assert.notNull(type, "Type must not be null!");
+
+ Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+ StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+ this.type = type;
+ }
+
+ /**
+ * Returns the {@link Type} of the binding.
+ *
+ * @return the type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Prepares the given raw keyword according to the like type.
+ */
+ @Nullable
+ @Override public Object prepare(@Nullable Object value) {
+ if (value == null)
+ return null;
+
+ switch (type) {
+ case STARTING_WITH:
+ return String.format("%s%%", value.toString());
+ case ENDING_WITH:
+ return String.format("%%%s", value.toString());
+ case CONTAINING:
+ return String.format("%%%s%%", value.toString());
+ case LIKE:
+ default:
+ return value;
+ }
+ }
+
+ /** */
+ @Override public boolean equals(Object obj) {
+ if (!(obj instanceof LikeParameterBinding))
+ return false;
+
+ LikeParameterBinding that = (LikeParameterBinding)obj;
+
+ return super.equals(obj) && type.equals(that.type);
+ }
+
+ /** */
+ @Override public int hashCode() {
+
+ int result = super.hashCode();
+
+ result += nullSafeHashCode(type);
+
+ return result;
+ }
+
+ /** */
+ @Override public String toString() {
+ return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
+ }
+
+ /**
+ * Extracts the like {@link Type} from the given JPA like expression.
+ *
+ * @param expression must not be {@literal null} or empty.
+ */
+ private static Type getLikeTypeFrom(String expression) {
+
+ Assert.hasText(expression, "Expression must not be null or empty!");
+
+ if (expression.matches("%.*%"))
+ return Type.CONTAINING;
+
+ if (expression.startsWith("%"))
+ return Type.ENDING_WITH;
+
+ if (expression.endsWith("%"))
+ return Type.STARTING_WITH;
+
+ return Type.LIKE;
+ }
+
+ }
+
+ /** */
+ static class Metadata {
+ /**
+ * Uses jdbc style parameters.
+ */
+ private boolean usesJdbcStyleParameters;
+ }
+
+ /**
+ * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers
+ * an API to query that information.
+ *
+ * @author Jens Schauder
+ * @author Oliver Gierke
+ */
+ static class QuotationMap {
+ /** */
+ private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
+
+ /** */
+ private final List<Range<Integer>> quotedRanges = new ArrayList<>();
+
+ /**
+ * Creates a new instance for the query.
+ *
+ * @param query can be {@literal null}.
+ */
+ public QuotationMap(@Nullable String query) {
+ if (query == null)
+ return;
+
+ Character inQuotation = null;
+ int start = 0;
+
+ for (int i = 0; i < query.length(); i++) {
+ char currentChar = query.charAt(i);
+
+ if (QUOTING_CHARACTERS.contains(currentChar)) {
+ if (inQuotation == null) {
+
+ inQuotation = currentChar;
+ start = i;
+ }
+ else if (currentChar == inQuotation) {
+ inQuotation = null;
+
+ quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
+ }
+ }
+ }
+
+ if (inQuotation != null) {
+ throw new IllegalArgumentException(
+ String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start));
+ }
+ }
+
+ /**
+ * Checks if a given index is within a quoted range.
+ *
+ * @param idx to check if it is part of a quoted range.
+ * @return whether the query contains a quoted range at {@literal index}.
+ */
+ public boolean isQuoted(int idx) {
+ return quotedRanges.stream().anyMatch(r -> r.contains(idx));
+ }
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/package-info.java
new file mode 100644
index 0000000..c9f90dc
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/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. -->
+ * Package includes classes that integrates with Apache Ignite SQL engine.
+ */
+package org.apache.ignite.springdata20.repository.query;
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelEvaluator.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelEvaluator.java
new file mode 100644
index 0000000..1c30673
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelEvaluator.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springdata20.repository.query.spel;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.ignite.springdata20.repository.query.spel.SpelQueryContext.SpelExtractor;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.Assert;
+
+/**
+ * Evaluates SpEL expressions as extracted by the {@link SpelExtractor} based on parameter information from a method and
+ * parameter values from a method call.
+ *
+ * @author Jens Schauder
+ * @author Gerrit Meier
+ * @author Oliver Gierke
+ */
+public class SpelEvaluator {
+ /** */
+ private static final SpelExpressionParser PARSER = new SpelExpressionParser();
+
+ /** */
+ private final EvaluationContextProvider evaluationCtxProvider;
+
+ /** */
+ private final Parameters<?, ?> parameters;
+
+ /** */
+ private final SpelExtractor extractor;
+
+ /**
+ * @param evaluationCtxProvider Evaluation context provider.
+ * @param parameters Parameters.
+ * @param extractor Extractor.
+ */
+ public SpelEvaluator(EvaluationContextProvider evaluationCtxProvider,
+ Parameters<?, ?> parameters,
+ SpelExtractor extractor) {
+ this.evaluationCtxProvider = evaluationCtxProvider;
+ this.parameters = parameters;
+ this.extractor = extractor;
+ }
+
+ /**
+ * Evaluate all the SpEL expressions in {@link #parameters} based on values provided as an argument.
+ *
+ * @param values Parameter values. Must not be {@literal null}.
+ * @return a map from parameter name to evaluated value. Guaranteed to be not {@literal null}.
+ */
+ public Map<String, Object> evaluate(Object[] values) {
+ Assert.notNull(values, "Values must not be null.");
+
+ EvaluationContext evaluationCtx = evaluationCtxProvider.getEvaluationContext(parameters, values);
+
+ return extractor.getParameters().collect(Collectors.toMap(//
+ Map.Entry::getKey, //
+ it -> getSpElValue(evaluationCtx, it.getValue()) //
+ ));
+ }
+
+ /**
+ * Returns the query string produced by the intermediate SpEL expression collection step.
+ *
+ * @return the query string produced by the intermediate SpEL expression collection step
+ */
+ public String getQueryString() {
+ return extractor.getQueryString();
+ }
+
+ /**
+ * @param evaluationCtx Evaluation context.
+ * @param expression Expression.
+ */
+ @Nullable
+ private static Object getSpElValue(EvaluationContext evaluationCtx, String expression) {
+ return PARSER.parseExpression(expression).getValue(evaluationCtx);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelQueryContext.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelQueryContext.java
new file mode 100644
index 0000000..40de67a
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelQueryContext.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springdata20.repository.query.spel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.util.Assert;
+
+/**
+ * Source of {@link SpelExtractor} encapsulating configuration often common for all queries.
+ *
+ * @author Jens Schauder
+ * @author Gerrit Meier
+ */
+public class SpelQueryContext {
+ /** */
+ private static final String SPEL_PATTERN_STRING = "([:?])#\\{([^}]+)}";
+
+ /** */
+ private static final Pattern SPEL_PATTERN = Pattern.compile(SPEL_PATTERN_STRING);
+
+ /**
+ * A function from the index of a SpEL expression in a query and the actual SpEL expression to the parameter name to
+ * be used in place of the SpEL expression. A typical implementation is expected to look like
+ * <code>(index, spel) -> "__some_placeholder_" + index</code>
+ */
+ private final BiFunction<Integer, String, String> paramNameSrc;
+
+ /**
+ * A function from a prefix used to demarcate a SpEL expression in a query and a parameter name as returned from
+ * {@link #paramNameSrc} to a {@literal String} to be used as a replacement of the SpEL in the query. The returned
+ * value should normally be interpretable as a bind parameter by the underlying persistence mechanism. A typical
+ * implementation is expected to look like <code>(prefix, name) -> prefix + name</code> or
+ * <code>(prefix, name) -> "{" + name + "}"</code>
+ */
+ private final BiFunction<String, String, String> replacementSrc;
+
+ /** */
+ private SpelQueryContext(BiFunction<Integer, String, String> paramNameSrc,
+ BiFunction<String, String, String> replacementSrc) {
+ this.paramNameSrc = paramNameSrc;
+ this.replacementSrc = replacementSrc;
+ }
+
+ /**
+ * Of spel query context.
+ *
+ * @param parameterNameSource the parameter name source
+ * @param replacementSource the replacement source
+ * @return the spel query context
+ */
+ public static SpelQueryContext of(BiFunction<Integer, String, String> parameterNameSource,
+ BiFunction<String, String, String> replacementSource) {
+ return new SpelQueryContext(parameterNameSource, replacementSource);
+ }
+
+ /**
+ * Parses the query for SpEL expressions using the pattern:
+ *
+ * <pre>
+ * <prefix>#{<spel>}
+ * </pre>
+ * <p>
+ * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double
+ * quotation marks.
+ *
+ * @param qry a query containing SpEL expressions in the format described above. Must not be {@literal null}.
+ * @return A {@link SpelExtractor} which makes the query with SpEL expressions replaced by bind parameters and a map
+ * from bind parameter to SpEL expression available. Guaranteed to be not {@literal null}.
+ */
+ public SpelExtractor parse(String qry) {
+ return new SpelExtractor(qry);
+ }
+
+ /**
+ * Createsa {@link EvaluatingSpelQueryContext} from the current one and the given {@link
+ * EvaluationContextProvider}*.
+ *
+ * @param provider must not be {@literal null}.
+ * @return Evaluating Spel QueryContext
+ */
+ public EvaluatingSpelQueryContext withEvaluationContextProvider(EvaluationContextProvider provider) {
+ Assert.notNull(provider, "EvaluationContextProvider must not be null!");
+
+ return new EvaluatingSpelQueryContext(provider, paramNameSrc, replacementSrc);
+ }
+
+ /**
+ * An extension of {@link SpelQueryContext} that can create {@link SpelEvaluator} instances as it also knows about a
+ * {@link EvaluationContextProvider}.
+ *
+ * @author Oliver Gierke
+ */
+ public static class EvaluatingSpelQueryContext extends SpelQueryContext {
+ /** */
+ private final EvaluationContextProvider evaluationContextProvider;
+
+ /**
+ * Creates a new {@link EvaluatingSpelQueryContext} for the given {@link EvaluationContextProvider}, parameter
+ * name source and replacement source.
+ *
+ * @param evaluationCtxProvider must not be {@literal null}.
+ * @param paramNameSrc must not be {@literal null}.
+ * @param replacementSrc must not be {@literal null}.
+ */
+ private EvaluatingSpelQueryContext(EvaluationContextProvider evaluationCtxProvider,
+ BiFunction<Integer, String, String> paramNameSrc, BiFunction<String, String, String> replacementSrc) {
+ super(paramNameSrc, replacementSrc);
+
+ evaluationContextProvider = evaluationCtxProvider;
+ }
+
+ /**
+ * Parses the query for SpEL expressions using the pattern:
+ *
+ * <pre>
+ * <prefix>#{<spel>}
+ * </pre>
+ * <p>
+ * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or
+ * double quotation marks.
+ *
+ * @param qry a query containing SpEL expressions in the format described above. Must not be {@literal
+ * null}.
+ * @param parameters a {@link Parameters} instance describing query method parameters
+ * @return A {@link SpelEvaluator} which allows to evaluate the SpEL expressions. Will never be {@literal null}.
+ */
+ public SpelEvaluator parse(String qry, Parameters<?, ?> parameters) {
+ return new SpelEvaluator(evaluationContextProvider, parameters, parse(qry));
+ }
+ }
+
+ /**
+ * Parses a query string, identifies the contained SpEL expressions, replaces them with bind parameters and offers a
+ * {@link Map} from those bind parameters to the spel expression.
+ * <p>
+ * The parser detects quoted parts of the query string and does not detect SpEL expressions inside such quoted parts
+ * of the query.
+ *
+ * @author Jens Schauder
+ * @author Oliver Gierke
+ */
+ public class SpelExtractor {
+ /** */
+ private static final int PREFIX_GROUP_INDEX = 1;
+
+ /** */
+ private static final int EXPRESSION_GROUP_INDEX = 2;
+
+ /** */
+ private final String query;
+
+ /** */
+ private final Map<String, String> expressions;
+
+ /** */
+ private final QuotationMap quotations;
+
+ /**
+ * Creates a SpelExtractor from a query String.
+ *
+ * @param qry must not be {@literal null}.
+ */
+ SpelExtractor(String qry) {
+ Assert.notNull(qry, "Query must not be null");
+
+ Map<String, String> exps = new HashMap<>();
+ Matcher matcher = SPEL_PATTERN.matcher(qry);
+ StringBuilder resultQry = new StringBuilder();
+ QuotationMap quotedAreas = new QuotationMap(qry);
+
+ int expressionCounter = 0;
+ int matchedUntil = 0;
+
+ while (matcher.find()) {
+ if (quotedAreas.isQuoted(matcher.start()))
+ resultQry.append(qry, matchedUntil, matcher.end());
+
+ else {
+ String spelExpression = matcher.group(EXPRESSION_GROUP_INDEX);
+ String prefix = matcher.group(PREFIX_GROUP_INDEX);
+
+ String paramName = paramNameSrc.apply(expressionCounter, spelExpression);
+ String replacement = replacementSrc.apply(prefix, paramName);
+
+ resultQry.append(qry, matchedUntil, matcher.start());
+ resultQry.append(replacement);
+
+ exps.put(paramName, spelExpression);
+ expressionCounter++;
+ }
+
+ matchedUntil = matcher.end();
+ }
+
+ resultQry.append(qry.substring(matchedUntil));
+
+ expressions = Collections.unmodifiableMap(exps);
+ query = resultQry.toString();
+ quotations = quotedAreas;
+ }
+
+ /**
+ * The query with all the SpEL expressions replaced with bind parameters.
+ *
+ * @return Guaranteed to be not {@literal null}.
+ */
+ public String getQueryString() {
+ return query;
+ }
+
+ /**
+ * Is quoted.
+ *
+ * @param idx the idx
+ * @return the boolean
+ */
+ public boolean isQuoted(int idx) {
+ return quotations.isQuoted(idx);
+ }
+
+ /**
+ * Gets parameter.
+ *
+ * @param name the name
+ * @return the parameter
+ */
+ public String getParameter(String name) {
+ return expressions.get(name);
+ }
+
+ /**
+ * A {@literal Map} from parameter name to SpEL expression.
+ *
+ * @return Guaranteed to be not {@literal null}.
+ */
+ Map<String, String> getParameterMap() {
+ return expressions;
+ }
+
+ /**
+ * Gets parameters.
+ *
+ * @return the parameters
+ */
+ Stream<Entry<String, String>> getParameters() {
+ return expressions.entrySet().stream();
+ }
+ }
+
+ /**
+ * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers
+ * an API to query that information.
+ *
+ * @author Jens Schauder
+ * @author Oliver Gierke
+ */
+ static class QuotationMap {
+ /** */
+ private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
+
+ /** */
+ private final List<Range<Integer>> quotedRanges = new ArrayList<>();
+
+ /**
+ * Creates a new {@link QuotationMap} for the query.
+ *
+ * @param qry can be {@literal null}.
+ */
+ public QuotationMap(@Nullable String qry) {
+ if (qry == null)
+ return;
+
+ Character inQuotation = null;
+ int start = 0;
+
+ for (int i = 0; i < qry.length(); i++) {
+
+ char curChar = qry.charAt(i);
+
+ if (QUOTING_CHARACTERS.contains(curChar)) {
+
+ if (inQuotation == null) {
+
+ inQuotation = curChar;
+ start = i;
+
+ }
+ else if (curChar == inQuotation) {
+
+ inQuotation = null;
+
+ quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
+ }
+ }
+ }
+
+ if (inQuotation != null) {
+ throw new IllegalArgumentException(
+ String.format("The string <%s> starts a quoted range at %d, but never ends it.", qry, start));
+ }
+ }
+
+ /**
+ * Checks if a given index is within a quoted range.
+ *
+ * @param idx to check if it is part of a quoted range.
+ * @return whether the query contains a quoted range at {@literal index}.
+ */
+ public boolean isQuoted(int idx) {
+ return quotedRanges.stream().anyMatch(r -> r.contains(idx));
+ }
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/ConditionFalse.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/ConditionFalse.java
new file mode 100644
index 0000000..1b4b378
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/ConditionFalse.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.springdata20.repository.support;
+
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+/**
+ * Always false condition. Tells spring context never load bean with such Condition.
+ */
+public class ConditionFalse implements Condition {
+ /** {@inheritDoc} */
+ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ return false;
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactory.java
new file mode 100644
index 0000000..87f637d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactory.java
@@ -0,0 +1,274 @@
+/*
+ * 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.springdata20.repository.support;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.apache.ignite.springdata20.repository.query.IgniteQuery;
+import org.apache.ignite.springdata20.repository.query.IgniteQueryGenerator;
+import org.apache.ignite.springdata20.repository.query.IgniteRepositoryQuery;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanExpressionContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.expression.StandardBeanExpressionResolver;
+import org.springframework.data.repository.core.EntityInformation;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractEntityInformation;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Crucial for spring-data functionality class. Create proxies for repositories.
+ * <p>
+ * Supports multiple Ignite Instances on same JVM.
+ * <p>
+ * This is pretty useful working with Spring repositories bound to different Ignite intances within same application.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteRepositoryFactory extends RepositoryFactorySupport {
+ /** Spring application context */
+ private final ApplicationContext ctx;
+
+ /** Spring application bean factory */
+ private final DefaultListableBeanFactory beanFactory;
+
+ /** Spring application expression resolver */
+ private final StandardBeanExpressionResolver resolver = new StandardBeanExpressionResolver();
+
+ /** Spring application bean expression context */
+ private final BeanExpressionContext beanExpressionContext;
+
+ /** Mapping of a repository to a cache. */
+ private final Map<Class<?>, String> repoToCache = new HashMap<>();
+
+ /** Mapping of a repository to a ignite instance. */
+ private final Map<Class<?>, Ignite> repoToIgnite = new HashMap<>();
+
+ /**
+ * Creates the factory with initialized {@link Ignite} instance.
+ *
+ * @param ctx the ctx
+ */
+ public IgniteRepositoryFactory(ApplicationContext ctx) {
+ this.ctx = ctx;
+
+ beanFactory = new DefaultListableBeanFactory(ctx.getAutowireCapableBeanFactory());
+
+ beanExpressionContext = new BeanExpressionContext(beanFactory, null);
+ }
+
+ /** */
+ private Ignite igniteForRepoConfig(RepositoryConfig config) {
+ try {
+ String igniteInstanceName = evaluateExpression(config.igniteInstance());
+ return (Ignite)ctx.getBean(igniteInstanceName);
+ }
+ catch (BeansException ex) {
+ try {
+ String igniteConfigName = evaluateExpression(config.igniteCfg());
+ IgniteConfiguration cfg = (IgniteConfiguration)ctx.getBean(igniteConfigName);
+ try {
+ // first try to attach to existing ignite instance
+ return Ignition.ignite(cfg.getIgniteInstanceName());
+ }
+ catch (Exception ignored) {
+ // nop
+ }
+ return Ignition.start(cfg);
+ }
+ catch (BeansException ex2) {
+ try {
+ String igniteSpringCfgPath = evaluateExpression(config.igniteSpringCfgPath());
+ String path = (String)ctx.getBean(igniteSpringCfgPath);
+ return Ignition.start(path);
+ }
+ catch (BeansException ex3) {
+ throw new IgniteException("Failed to initialize Ignite repository factory. Ignite instance or"
+ + " IgniteConfiguration or a path to Ignite's spring XML "
+ + "configuration must be defined in the"
+ + " application configuration");
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
+ return new AbstractEntityInformation<T, ID>(domainClass) {
+ /** {@inheritDoc} */
+ @Override public ID getId(T entity) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Class<ID> getIdType() {
+ return null;
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
+ return IgniteRepositoryImpl.class;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected synchronized RepositoryMetadata getRepositoryMetadata(Class<?> repoItf) {
+ Assert.notNull(repoItf, "Repository interface must be set.");
+ Assert.isAssignable(IgniteRepository.class, repoItf, "Repository must implement IgniteRepository interface.");
+
+ RepositoryConfig annotation = repoItf.getAnnotation(RepositoryConfig.class);
+
+ Assert.notNull(annotation, "Set a name of an Apache Ignite cache using @RepositoryConfig annotation to map "
+ + "this repository to the underlying cache.");
+
+ Assert.hasText(annotation.cacheName(), "Set a name of an Apache Ignite cache using @RepositoryConfig "
+ + "annotation to map this repository to the underlying cache.");
+
+ String cacheName = evaluateExpression(annotation.cacheName());
+
+ repoToCache.put(repoItf, cacheName);
+
+ repoToIgnite.put(repoItf, igniteForRepoConfig(annotation));
+
+ return super.getRepositoryMetadata(repoItf);
+ }
+
+ /**
+ * Evaluate the SpEL expression
+ *
+ * @param spelExpression SpEL expression
+ * @return the result of execution of the SpEL expression
+ */
+ private String evaluateExpression(String spelExpression) {
+ return (String)resolver.evaluate(spelExpression, beanExpressionContext);
+ }
+
+ /** Control underlying cache creation to avoid cache creation by mistake */
+ private IgniteCache getRepositoryCache(Class<?> repoIf) {
+ Ignite ignite = repoToIgnite.get(repoIf);
+
+ RepositoryConfig config = repoIf.getAnnotation(RepositoryConfig.class);
+
+ String cacheName = repoToCache.get(repoIf);
+
+ IgniteCache c = config.autoCreateCache() ? ignite.getOrCreateCache(cacheName) : ignite.cache(cacheName);
+
+ if (c == null) {
+ throw new IllegalStateException(
+ "Cache '" + cacheName + "' not found for repository interface " + repoIf.getName()
+ + ". Please, add a cache configuration to ignite configuration"
+ + " or pass autoCreateCache=true to org.apache.ignite.springdata20"
+ + ".repository.config.RepositoryConfig annotation.");
+ }
+
+ return c;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Object getTargetRepository(RepositoryInformation metadata) {
+ Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+ return getTargetRepositoryViaReflection(metadata, ignite,
+ getRepositoryCache(metadata.getRepositoryInterface()));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(final QueryLookupStrategy.Key key,
+ EvaluationContextProvider evaluationContextProvider) {
+ return Optional.of((mtd, metadata, factory, namedQueries) -> {
+ final Query annotation = mtd.getAnnotation(Query.class);
+ final Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+ if (annotation != null && (StringUtils.hasText(annotation.value()) || annotation.textQuery() || annotation
+ .dynamicQuery())) {
+
+ String qryStr = annotation.value();
+
+ boolean annotatedIgniteQuery = !annotation.dynamicQuery() && (StringUtils.hasText(qryStr) || annotation
+ .textQuery());
+
+ IgniteQuery query = annotatedIgniteQuery ? new IgniteQuery(qryStr,
+ !annotation.textQuery() && (isFieldQuery(qryStr) || annotation.forceFieldsQuery()),
+ annotation.textQuery(), false, IgniteQueryGenerator.getOptions(mtd)) : null;
+
+ if (key != QueryLookupStrategy.Key.CREATE) {
+ return new IgniteRepositoryQuery(ignite, metadata, query, mtd, factory,
+ getRepositoryCache(metadata.getRepositoryInterface()),
+ annotatedIgniteQuery ? DynamicQueryConfig.fromQueryAnnotation(annotation) : null,
+ evaluationContextProvider);
+ }
+ }
+
+ if (key == QueryLookupStrategy.Key.USE_DECLARED_QUERY) {
+ throw new IllegalStateException("To use QueryLookupStrategy.Key.USE_DECLARED_QUERY, pass "
+ + "a query string via org.apache.ignite.springdata20.repository"
+ + ".config.Query annotation.");
+ }
+
+ return new IgniteRepositoryQuery(ignite, metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd,
+ factory, getRepositoryCache(metadata.getRepositoryInterface()),
+ DynamicQueryConfig.fromQueryAnnotation(annotation), evaluationContextProvider);
+ });
+ }
+
+ /**
+ * @param qry Query string.
+ * @return {@code true} if query is SqlFieldsQuery.
+ */
+ public static boolean isFieldQuery(String qry) {
+ String qryUpperCase = qry.toUpperCase();
+
+ return isStatement(qryUpperCase) && !qryUpperCase.matches("^SELECT\\s+(?:\\w+\\.)?+\\*.*");
+ }
+
+ /**
+ * Evaluates if the query starts with a clause.<br>
+ * <code>SELECT, INSERT, UPDATE, MERGE, DELETE</code>
+ *
+ * @param qryUpperCase Query string in upper case.
+ * @return {@code true} if query is full SQL statement.
+ */
+ private static boolean isStatement(String qryUpperCase) {
+ return qryUpperCase.matches("^\\s*SELECT\\b.*") ||
+ // update
+ qryUpperCase.matches("^\\s*UPDATE\\b.*") ||
+ // delete
+ qryUpperCase.matches("^\\s*DELETE\\b.*") ||
+ // merge
+ qryUpperCase.matches("^\\s*MERGE\\b.*") ||
+ // insert
+ qryUpperCase.matches("^\\s*INSERT\\b.*");
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactoryBean.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactoryBean.java
new file mode 100644
index 0000000..5b3d612
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactoryBean.java
@@ -0,0 +1,67 @@
+/*
+ * 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.springdata20.repository.support;
+
+import java.io.Serializable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Apache Ignite repository factory bean.
+ * <p>
+ * The {@link org.apache.ignite.springdata20.repository.config.RepositoryConfig} requires to define one of the
+ * parameters below in your Spring application configuration in order to get an access to Apache Ignite cluster:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance" by default</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg" by default</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath" by default</li>
+ * <ul/>
+ *
+ * @param <T> Repository type, {@link IgniteRepository}
+ * @param <V> Domain object class.
+ * @param <K> Domain object key, super expects {@link Serializable}.
+ */
+public class IgniteRepositoryFactoryBean<T extends Repository<V, K>, V, K extends Serializable>
+ extends RepositoryFactoryBeanSupport<T, V, K> implements ApplicationContextAware {
+ /** */
+ private ApplicationContext ctx;
+
+ /**
+ * @param repoInterface Repository interface.
+ */
+ protected IgniteRepositoryFactoryBean(Class<? extends T> repoInterface) {
+ super(repoInterface);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void setApplicationContext(ApplicationContext ctx) throws BeansException {
+ this.ctx = ctx;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryFactorySupport createRepositoryFactory() {
+ return new IgniteRepositoryFactory(ctx);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryImpl.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryImpl.java
new file mode 100644
index 0000000..50a7d4c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryImpl.java
@@ -0,0 +1,221 @@
+/*
+ * 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.springdata20.repository.support;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.cache.Cache;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * General Apache Ignite repository implementation. This bean should've never been loaded by context directly, only via
+ * {@link IgniteRepositoryFactory}
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Conditional(ConditionFalse.class)
+public class IgniteRepositoryImpl<V, K extends Serializable> implements IgniteRepository<V, K> {
+ /**
+ * Ignite Cache bound to the repository
+ */
+ private final IgniteCache<K, V> cache;
+
+ /**
+ * Ignite instance bound to the repository
+ */
+ private final Ignite ignite;
+
+ /**
+ * Repository constructor.
+ *
+ * @param ignite the ignite
+ * @param cache Initialized cache instance.
+ */
+ public IgniteRepositoryImpl(Ignite ignite, IgniteCache<K, V> cache) {
+ this.cache = cache;
+ this.ignite = ignite;
+ }
+
+ /** {@inheritDoc} */
+ @Override public IgniteCache<K, V> cache() {
+ return cache;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Ignite ignite() {
+ return ignite;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> S save(K key, S entity) {
+ cache.put(key, entity);
+
+ return entity;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> Iterable<S> save(Map<K, S> entities) {
+ cache.putAll(entities);
+
+ return entities.values();
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc) {
+ if (expiryPlc != null)
+ cache.withExpiryPolicy(expiryPlc).put(key, entity);
+ else
+ cache.put(key, entity);
+ return entity;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc) {
+ if (expiryPlc != null)
+ cache.withExpiryPolicy(expiryPlc).putAll(entities);
+ else
+ cache.putAll(entities);
+ return entities.values();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override public <S extends V> S save(S entity) {
+ throw new UnsupportedOperationException("Use IgniteRepository.save(key,value) method instead.");
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override public <S extends V> Iterable<S> saveAll(Iterable<S> entities) {
+ throw new UnsupportedOperationException("Use IgniteRepository.save(Map<keys,value>) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public Optional<V> findById(K id) {
+ return Optional.ofNullable(cache.get(id));
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean existsById(K id) {
+ return cache.containsKey(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterable<V> findAll() {
+ final Iterator<Cache.Entry<K, V>> iter = cache.iterator();
+
+ return new Iterable<V>() {
+ /** */
+ @Override public Iterator<V> iterator() {
+ return new Iterator<V>() {
+ /** {@inheritDoc} */
+ @Override public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ /** {@inheritDoc} */
+ @Override public V next() {
+ return iter.next().getValue();
+ }
+
+ /** {@inheritDoc} */
+ @Override public void remove() {
+ iter.remove();
+ }
+ };
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterable<V> findAllById(Iterable<K> ids) {
+ if (ids instanceof Set)
+ return cache.getAll((Set<K>)ids).values();
+
+ if (ids instanceof Collection)
+ return cache.getAll(new HashSet<>((Collection<K>)ids)).values();
+
+ TreeSet<K> keys = new TreeSet<>();
+
+ for (K id : ids)
+ keys.add(id);
+
+ return cache.getAll(keys).values();
+ }
+
+ /** {@inheritDoc} */
+ @Override public long count() {
+ return cache.size(CachePeekMode.PRIMARY);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteById(K id) {
+ cache.remove(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(V entity) {
+ throw new UnsupportedOperationException("Use IgniteRepository.deleteById(key) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAll(Iterable<? extends V> entities) {
+ throw new UnsupportedOperationException("Use IgniteRepository.deleteAllById(keys) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAllById(Iterable<K> ids) {
+ if (ids instanceof Set) {
+ cache.removeAll((Set<K>)ids);
+ return;
+ }
+
+ if (ids instanceof Collection) {
+ cache.removeAll(new HashSet<>((Collection<K>)ids));
+ return;
+ }
+
+ TreeSet<K> keys = new TreeSet<>();
+
+ for (K id : ids)
+ keys.add(id);
+
+ cache.removeAll(keys);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAll() {
+ cache.clear();
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/package-info.java
new file mode 100644
index 0000000..a9d4fd2
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/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. -->
+ * Package contains supporting files required by Spring Data framework.
+ */
+package org.apache.ignite.springdata20.repository.support;
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
new file mode 100644
index 0000000..6a69f32
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.springdata;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.compoundkey.City;
+import org.apache.ignite.springdata.compoundkey.CityKey;
+import org.apache.ignite.springdata.compoundkey.CityRepository;
+import org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+import static org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration.CLI_CONN_PORT;
+
+/**
+ * Test with using conpoud key in spring-data
+ * */
+public class IgniteSpringDataCompoundKeyTest extends GridCommonAbstractTest {
+ /** Application context */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** City repository */
+ private static CityRepository repo;
+
+ /** Cache name */
+ private static final String CACHE_NAME = "City";
+
+ /** Cities count */
+ private static final int TOTAL_COUNT = 5;
+
+ /** Count Afganistan cities */
+ private static final int AFG_COUNT = 4;
+
+ /** Kabul identifier */
+ private static final int KABUL_ID = 1;
+
+ /** Quandahar identifier */
+ private static final int QUANDAHAR_ID = 2;
+
+ /** Afganistan county code */
+ private static final String AFG = "AFG";
+
+ /** test city Kabul */
+ private static final City KABUL = new City("Kabul", "Kabol", 1780000);
+
+ /** test city Quandahar */
+ private static final City QUANDAHAR = new City("Qandahar","Qandahar", 237500);
+
+ /**
+ * Performs context initialization before tests.
+ */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+ ctx.register(CompoundKeyApplicationConfiguration.class);
+ ctx.refresh();
+
+ repo = ctx.getBean(CityRepository.class);
+ }
+
+ /**
+ * Load data
+ * */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ loadData();
+
+ assertEquals(TOTAL_COUNT, repo.count());
+ }
+
+ /**
+ * Performs context destroy after tests.
+ */
+ @Override protected void afterTestsStopped() {
+ ctx.close();
+ }
+
+ /** load data*/
+ public void loadData() throws Exception {
+ Ignite ignite = ctx.getBean(Ignite.class);
+
+ if (ignite.cacheNames().contains(CACHE_NAME))
+ ignite.destroyCache(CACHE_NAME);
+
+ try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:" + CLI_CONN_PORT + '/')) {
+ Statement st = conn.createStatement();
+
+ st.execute("DROP TABLE IF EXISTS City");
+ st.execute("CREATE TABLE City (ID INT, Name VARCHAR, CountryCode CHAR(3), District VARCHAR, Population INT, PRIMARY KEY (ID, CountryCode)) WITH \"template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, KEY_TYPE=org.apache.ignite.springdata.compoundkey.CityKey, VALUE_TYPE=org.apache.ignite.springdata.compoundkey.City\"");
+ st.execute("SET STREAMING ON;");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (1,'Kabul','AFG','Kabol',1780000)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (2,'Qandahar','AFG','Qandahar',237500)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (3,'Herat','AFG','Herat',186800)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (4,'Mazar-e-Sharif','AFG','Balkh',127800)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (5,'Amsterdam','NLD','Noord-Holland',731200)");
+ }
+ }
+
+ /** Test */
+ @Test
+ public void test() {
+ assertEquals(Optional.of(KABUL), repo.findById(new CityKey(KABUL_ID, AFG)));
+ assertEquals(AFG_COUNT, repo.findByCountryCode(AFG).size());
+ assertEquals(QUANDAHAR, repo.findById(QUANDAHAR_ID));
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
new file mode 100644
index 0000000..5841f41
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.springdata;
+
+import java.util.Collection;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonExpressionRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * Test with using repository which is configured by Spring EL
+ */
+public class IgniteSpringDataCrudSelfExpressionTest extends GridCommonAbstractTest {
+ /** Number of entries to store */
+ private static final int CACHE_SIZE = 1000;
+
+ /** Repository. */
+ private static PersonExpressionRepository repo;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** */
+ @Rule
+ public final ExpectedException expected = ExpectedException.none();
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+ ctx.register(ApplicationConfiguration.class);
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonExpressionRepository.class);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ fillInRepository();
+
+ assertEquals(CACHE_SIZE, repo.count());
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+
+ super.afterTest();
+ }
+
+ /** */
+ private void fillInRepository() {
+ for (int i = 0; i < CACHE_SIZE - 5; i++) {
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+
+ repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() {
+ ctx.close();
+ }
+
+ /**
+ * Tests put & get operations.
+ */
+ @Test
+ public void testPutGet() {
+ Person person = new Person("some_name", "some_surname");
+
+ int id = CACHE_SIZE + 1;
+
+ assertEquals(person, repo.save(id, person));
+
+ assertTrue(repo.existsById(id));
+
+ assertEquals(person, repo.findById(id).get());
+
+ expected.expect(UnsupportedOperationException.class);
+ expected.expectMessage("Use IgniteRepository.save(key,value) method instead.");
+ repo.save(person);
+ }
+
+ /**
+ * Tests SpEL expression.
+ */
+ @Test
+ public void testCacheCount() {
+ Ignite ignite = ctx.getBean("igniteInstance", Ignite.class);
+
+ Collection<String> cacheNames = ignite.cacheNames();
+
+ assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+ cacheNames.contains("#{cacheNames.personCacheName}"));
+
+ assertTrue("Cache \"PersonCache\" isn't found!",
+ cacheNames.contains("PersonCache"));
+ }
+
+ /** */
+ @Test
+ public void testCacheCountTWO() {
+ Ignite ignite = ctx.getBean("igniteInstanceTWO", Ignite.class);
+
+ Collection<String> cacheNames = ignite.cacheNames();
+
+ assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+ cacheNames.contains("#{cacheNames.personCacheName}"));
+
+ assertTrue("Cache \"PersonCache\" isn't found!",
+ cacheNames.contains("PersonCache"));
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
new file mode 100644
index 0000000..38785ef
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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.springdata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.TreeSet;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.FullNameProjection;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * CRUD tests.
+ */
+public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
+ /** Repository. */
+ private static PersonRepository repo;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** Number of entries to store */
+ private static int CACHE_SIZE = 1000;
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+
+ ctx.register(ApplicationConfiguration.class);
+
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonRepository.class);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ fillInRepository();
+
+ assertEquals(CACHE_SIZE, repo.count());
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+
+ super.afterTest();
+ }
+
+ /** */
+ private void fillInRepository() {
+ for (int i = 0; i < CACHE_SIZE - 5; i++) {
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+
+ repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() throws Exception {
+ ctx.destroy();
+ }
+
+ /** */
+ @Test
+ public void testPutGet() {
+ Person person = new Person("some_name", "some_surname");
+
+ int id = CACHE_SIZE + 1;
+
+ assertEquals(person, repo.save(id, person));
+
+ assertTrue(repo.existsById(id));
+
+ assertEquals(person, repo.findById(id).get());
+
+ try {
+ repo.save(person);
+
+ fail("Managed to save a Person without ID");
+ }
+ catch (UnsupportedOperationException e) {
+ //excepted
+ }
+ }
+
+ /** */
+ @Test
+ public void testPutAllGetAll() {
+ LinkedHashMap<Integer, Person> map = new LinkedHashMap<>();
+
+ for (int i = CACHE_SIZE; i < CACHE_SIZE + 50; i++)
+ map.put(i, new Person("some_name" + i, "some_surname" + i));
+
+ Iterator<Person> persons = repo.save(map).iterator();
+
+ assertEquals(CACHE_SIZE + 50, repo.count());
+
+ Iterator<Person> origPersons = map.values().iterator();
+
+ while (persons.hasNext())
+ assertEquals(origPersons.next(), persons.next());
+
+ try {
+ repo.saveAll(map.values());
+
+ fail("Managed to save a list of Persons with ids");
+ }
+ catch (UnsupportedOperationException e) {
+ //expected
+ }
+
+ persons = repo.findAllById(map.keySet()).iterator();
+
+ int counter = 0;
+
+ while (persons.hasNext()) {
+ persons.next();
+ counter++;
+ }
+
+ assertEquals(map.size(), counter);
+ }
+
+ /** */
+ @Test
+ public void testGetAll() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ Iterator<Person> persons = repo.findAll().iterator();
+
+ int counter = 0;
+
+ while (persons.hasNext()) {
+ persons.next();
+ counter++;
+ }
+
+ assertEquals(repo.count(), counter);
+ }
+
+ /** */
+ @Test
+ public void testDelete() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ repo.deleteById(0);
+
+ assertEquals(CACHE_SIZE - 1, repo.count());
+ assertEquals(Optional.empty(),repo.findById(0));
+
+ try {
+ repo.delete(new Person("", ""));
+
+ fail("Managed to delete a Person without id");
+ }
+ catch (UnsupportedOperationException e) {
+ //expected
+ }
+ }
+
+ /** */
+ @Test
+ public void testDeleteSet() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ TreeSet<Integer> ids = new TreeSet<>();
+
+ for (int i = 0; i < CACHE_SIZE / 2; i++)
+ ids.add(i);
+
+ repo.deleteAllById(ids);
+
+ assertEquals(CACHE_SIZE / 2, repo.count());
+
+ try {
+ ArrayList<Person> persons = new ArrayList<>();
+
+ for (int i = 0; i < 3; i++)
+ persons.add(new Person(String.valueOf(i), String.valueOf(i)));
+
+ repo.deleteAll(persons);
+
+ fail("Managed to delete Persons without ids");
+ }
+ catch (UnsupportedOperationException e) {
+ //expected
+ }
+ }
+
+ /** */
+ @Test
+ public void testDeleteAll() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+ }
+
+ /**
+ * Delete existing record.
+ */
+ @Test
+ public void testDeleteByFirstName() {
+ assertEquals(repo.countByFirstNameLike("uniquePerson"), 1);
+
+ long cnt = repo.deleteByFirstName("uniquePerson");
+
+ assertEquals(1, cnt);
+ }
+
+ /**
+ * Delete NON existing record.
+ */
+ @Test
+ public void testDeleteExpression() {
+ long cnt = repo.deleteByFirstName("880");
+
+ assertEquals(0, cnt);
+ }
+
+ /**
+ * Delete Multiple records due to where.
+ */
+ @Test
+ public void testDeleteExpressionMultiple() {
+ long count = repo.countByFirstName("nonUniquePerson");
+ long cnt = repo.deleteByFirstName("nonUniquePerson");
+
+ assertEquals(cnt, count);
+ }
+
+ /**
+ * Remove should do the same than Delete.
+ */
+ @Test
+ public void testRemoveExpression() {
+ repo.removeByFirstName("person3f");
+
+ long count = repo.count();
+ assertEquals(CACHE_SIZE - 1, count);
+ }
+
+ /**
+ * Delete unique record using lower case key word.
+ */
+ @Test
+ public void testDeleteQuery() {
+ repo.deleteBySecondNameLowerCase("uniqueLastName");
+
+ long countAfter = repo.count();
+ assertEquals(CACHE_SIZE - 1, countAfter);
+ }
+
+ /**
+ * Try to delete with a wrong @Query.
+ */
+ @Test
+ public void testWrongDeleteQuery() {
+ long countBefore = repo.countByFirstNameLike("person3f");
+
+ try {
+ repo.deleteWrongByFirstNameQuery("person3f");
+ }
+ catch (Exception e) {
+ //expected
+ }
+
+ long countAfter = repo.countByFirstNameLike("person3f");
+ assertEquals(countBefore, countAfter);
+ }
+
+ /**
+ * Update with a @Query a record.
+ */
+ @Test
+ public void testUpdateQueryMixedCase() {
+ final String newSecondName = "updatedUniqueSecondName";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<Person> person = repo.findByFirstName("uniquePerson");
+ assertEquals(person.get(0).getSecondName(), "updatedUniqueSecondName");
+ }
+
+ /**
+ * Update with a @Query a record
+ */
+ @Test
+ public void testUpdateQueryMixedCaseProjection() {
+ final String newSecondName = "updatedUniqueSecondName1";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjection("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName1");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionNamedParameter() {
+ final String newSecondName = "updatedUniqueSecondName2";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseDynamicProjectionNamedParameter() {
+ final String newSecondName = "updatedUniqueSecondName2";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+ List<FullNameProjection> personFullName = repo.queryByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+ assertEquals(personFullName.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryOneMixedCaseDynamicProjectionNamedParameter() {
+ final String newSecondName = "updatedUniqueSecondName2";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ PersonProjection person = repo.queryOneByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+ assertEquals(person.getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+ FullNameProjection personFullName = repo.queryOneByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+ assertEquals(personFullName.getFullName(), "uniquePerson updatedUniqueSecondName2");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionIndexedParameter() {
+ final String newSecondName = "updatedUniqueSecondName3";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedIndexedParameter("notUsed","uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName3");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionIndexedParameterLuceneTextQuery() {
+ final String newSecondName = "updatedUniqueSecondName4";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.textQueryByFirstNameWithProjectionNamedParameter("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName4");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionNamedParameterAndTemplateDomainEntityVariable() {
+ final String newSecondName = "updatedUniqueSecondName5";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName5");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionNamedParameterWithSpELExtension() {
+ final String newSecondName = "updatedUniqueSecondName6";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterWithSpELExtension("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName6");
+ assertEquals(person.get(0).getFirstName(), person.get(0).getFirstNameTransformed());
+ }
+
+ /**
+ * Update with a wrong @Query
+ */
+ @Test
+ public void testWrongUpdateQuery() {
+ final String newSecondName = "updatedUniqueSecondName";
+ int rowsUpdated = 0;
+
+ try {
+ rowsUpdated = repo.setWrongFixedSecondName(newSecondName, "uniquePerson");
+ }
+ catch (Exception ignored) {
+ //expected
+ }
+
+ assertEquals(0, rowsUpdated);
+
+ List<Person> person = repo.findByFirstName("uniquePerson");
+ assertEquals(person.get(0).getSecondName(), "uniqueLastName");
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
new file mode 100644
index 0000000..9c16f6d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
@@ -0,0 +1,409 @@
+/*
+ * 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.springdata;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonRepositoryOtherIgniteInstance;
+import org.apache.ignite.springdata.misc.PersonSecondRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+/**
+ *
+ */
+public class IgniteSpringDataQueriesSelfTest extends GridCommonAbstractTest {
+ /** Repository. */
+ private static PersonRepository repo;
+
+ /** Repository 2. */
+ private static PersonSecondRepository repo2;
+
+ /**
+ * Repository Ignite Instance cluster TWO.
+ */
+ private static PersonRepositoryOtherIgniteInstance repoTWO;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /**
+ * Number of entries to store
+ */
+ private static int CACHE_SIZE = 1000;
+
+ /**
+ * Performs context initialization before tests.
+ */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+
+ ctx.register(ApplicationConfiguration.class);
+
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonRepository.class);
+ repo2 = ctx.getBean(PersonSecondRepository.class);
+ // repository on another ignite instance (and another cluster)
+ repoTWO = ctx.getBean(PersonRepositoryOtherIgniteInstance.class);
+
+ for (int i = 0; i < CACHE_SIZE; i++) {
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ repoTWO.save(i, new Person("TWOperson" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+ }
+
+ /**
+ * Performs context destroy after tests.
+ */
+ @Override protected void afterTestsStopped() throws Exception {
+ ctx.destroy();
+ }
+
+ /** */
+ @Test
+ public void testExplicitQuery() {
+ List<Person> persons = repo.simpleQuery("person4a");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("person4a", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testExplicitQueryTWO() {
+ List<Person> persons = repoTWO.simpleQuery("TWOperson4a");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("TWOperson4a", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testEqualsPart() {
+ List<Person> persons = repo.findByFirstName("person4e");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("person4e", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testEqualsPartTWO() {
+ List<Person> persons = repoTWO.findByFirstName("TWOperson4e");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("TWOperson4e", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testContainingPart() {
+ List<Person> persons = repo.findByFirstNameContaining("person4");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertTrue(person.getFirstName().startsWith("person4"));
+ }
+
+ /** */
+ @Test
+ public void testContainingPartTWO() {
+ List<Person> persons = repoTWO.findByFirstNameContaining("TWOperson4");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertTrue(person.getFirstName().startsWith("TWOperson4"));
+ }
+
+ /** */
+ @Test
+ public void testTopPart() {
+ Iterable<Person> top = repo.findTopByFirstNameContaining("person4");
+
+ Iterator<Person> iter = top.iterator();
+
+ Person person = iter.next();
+
+ assertFalse(iter.hasNext());
+
+ assertTrue(person.getFirstName().startsWith("person4"));
+ }
+
+ /** */
+ @Test
+ public void testTopPartTWO() {
+ Iterable<Person> top = repoTWO.findTopByFirstNameContaining("TWOperson4");
+
+ Iterator<Person> iter = top.iterator();
+
+ Person person = iter.next();
+
+ assertFalse(iter.hasNext());
+
+ assertTrue(person.getFirstName().startsWith("TWOperson4"));
+ }
+
+ /** */
+ @Test
+ public void testLikeAndLimit() {
+ Iterable<Person> like = repo.findFirst10ByFirstNameLike("person");
+
+ int cnt = 0;
+
+ for (Person next : like) {
+ assertTrue(next.getFirstName().contains("person"));
+
+ cnt++;
+ }
+
+ assertEquals(10, cnt);
+ }
+
+ /** */
+ @Test
+ public void testLikeAndLimitTWO() {
+ Iterable<Person> like = repoTWO.findFirst10ByFirstNameLike("TWOperson");
+
+ int cnt = 0;
+
+ for (Person next : like) {
+ assertTrue(next.getFirstName().contains("TWOperson"));
+
+ cnt++;
+ }
+
+ assertEquals(10, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCount() {
+ int cnt = repo.countByFirstNameLike("person");
+
+ assertEquals(1000, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCountTWO() {
+ int cnt = repoTWO.countByFirstNameLike("TWOperson");
+
+ assertEquals(1000, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCount2() {
+ int cnt = repo.countByFirstNameLike("person4");
+
+ assertTrue(cnt < 1000);
+ }
+
+ /** */
+ @Test
+ public void testCount2TWO() {
+ int cnt = repoTWO.countByFirstNameLike("TWOperson4");
+
+ assertTrue(cnt < 1000);
+ }
+
+ /** */
+ @Test
+ public void testPageable() {
+ PageRequest pageable = new PageRequest(1, 5, Sort.Direction.DESC, "firstName");
+
+ HashSet<String> firstNames = new HashSet<>();
+
+ List<Person> pageable1 = repo.findByFirstNameRegex("^[a-z]+$", pageable);
+
+ assertEquals(5, pageable1.size());
+
+ for (Person person : pageable1) {
+ firstNames.add(person.getFirstName());
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+ }
+
+ List<Person> pageable2 = repo.findByFirstNameRegex("^[a-z]+$", pageable.next());
+
+ assertEquals(5, pageable2.size());
+
+ for (Person person : pageable2) {
+ firstNames.add(person.getFirstName());
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+ }
+
+ assertEquals(10, firstNames.size());
+ }
+
+ /** */
+ @Test
+ public void testAndAndOr() {
+ int cntAnd = repo.countByFirstNameLikeAndSecondNameLike("person1", "lastName1");
+
+ int cntOr = repo.countByFirstNameStartingWithOrSecondNameStartingWith("person1", "lastName1");
+
+ assertTrue(cntAnd <= cntOr);
+ }
+
+ /** */
+ @Test
+ public void testQueryWithSort() {
+ List<Person> persons = repo.queryWithSort("^[a-z]+$", new Sort(Sort.Direction.DESC, "secondName"));
+
+ Person previous = persons.get(0);
+
+ for (Person person : persons) {
+ assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+ previous = person;
+ }
+ }
+
+ /** */
+ @Test
+ public void testQueryWithPaging() {
+ List<Person> persons = repo.queryWithPageable("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+ assertEquals(7, persons.size());
+
+ Person previous = persons.get(0);
+
+ for (Person person : persons) {
+ assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+ previous = person;
+ }
+ }
+
+ /** */
+ @Test
+ public void testQueryFields() {
+ List<String> persons = repo.selectField("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+ assertEquals(7, persons.size());
+ }
+
+ /** */
+ @Test
+ public void testFindCacheEntries() {
+ List<Cache.Entry<Integer, Person>> cacheEntries = repo.findBySecondNameLike("stName1");
+
+ assertFalse(cacheEntries.isEmpty());
+
+ for (Cache.Entry<Integer, Person> entry : cacheEntries)
+ assertTrue(entry.getValue().getSecondName().contains("stName1"));
+ }
+
+ /** */
+ @Test
+ public void testFindOneCacheEntry() {
+ Cache.Entry<Integer, Person> cacheEntry = repo.findTopBySecondNameLike("tName18");
+
+ assertNotNull(cacheEntry);
+
+ assertTrue(cacheEntry.getValue().getSecondName().contains("tName18"));
+ }
+
+ /** */
+ @Test
+ public void testFindOneValue() {
+ PersonProjection person = repo.findTopBySecondNameStartingWith("lastName18");
+
+ assertNotNull(person);
+
+ assertTrue(person.getFullName().split("\\s")[1].startsWith("lastName18"));
+ }
+
+ /** */
+ @Test
+ public void testSelectSeveralFields() {
+ List<List> lists = repo.selectSeveralField("^[a-z]+$", new PageRequest(2, 6));
+
+ assertEquals(6, lists.size());
+
+ for (List list : lists) {
+ assertEquals(2, list.size());
+
+ assertTrue(list.get(0) instanceof Integer);
+ }
+ }
+
+ /** */
+ @Test
+ public void testCountQuery() {
+ int cnt = repo.countQuery(".*");
+
+ assertEquals(256, cnt);
+ }
+
+ /** */
+ @Test
+ public void testSliceOfCacheEntries() {
+ Slice<Cache.Entry<Integer, Person>> slice = repo2.findBySecondNameIsNot("lastName18", new PageRequest(3, 4));
+
+ assertEquals(4, slice.getSize());
+
+ for (Cache.Entry<Integer, Person> entry : slice)
+ assertFalse("lastName18".equals(entry.getValue().getSecondName()));
+ }
+
+ /** */
+ @Test
+ public void testSliceOfLists() {
+ Slice<List> lists = repo2.querySliceOfList("^[a-z]+$", new PageRequest(0, 3));
+
+ assertEquals(3, lists.getSize());
+
+ for (List list : lists) {
+ assertEquals(2, list.size());
+
+ assertTrue(list.get(0) instanceof Integer);
+ }
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
new file mode 100644
index 0000000..e86a575
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.Objects;
+
+/**
+ * Value-class
+ * */
+public class City {
+ /** City name */
+ private String name;
+
+ /** City district */
+ private String district;
+
+ /** City population */
+ private int population;
+
+ /**
+ * @param name city name
+ * @param district city district
+ * @param population city population
+ * */
+ public City(String name, String district, int population) {
+ this.name = name;
+ this.district = district;
+ this.population = population;
+ }
+
+ /**
+ * @return city name
+ * */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name city name
+ * */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return city district
+ * */
+ public String getDistrict() {
+ return district;
+ }
+
+ /**
+ * @param district city district
+ * */
+ public void setDistrict(String district) {
+ this.district = district;
+ }
+
+ /**
+ * @return city population
+ * */
+ public int getPopulation() {
+ return population;
+ }
+
+ /**
+ * @param population city population
+ * */
+ public void setPopulation(int population) {
+ this.population = population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return name + " | " + district + " | " + population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ City city = (City)o;
+
+ return
+ Objects.equals(this.name, city.name) &&
+ Objects.equals(this.district, city.district) &&
+ this.population == city.population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(name, district, population);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
new file mode 100644
index 0000000..8a5dba0
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
@@ -0,0 +1,76 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/** Compound key for city class */
+public class CityKey implements Serializable {
+ /** city identifier */
+ private int ID;
+
+ /** affinity key countrycode */
+ @AffinityKeyMapped
+ private String COUNTRYCODE;
+
+ /**
+ * @param id city identifier
+ * @param countryCode city countrycode
+ * */
+ public CityKey(int id, String countryCode) {
+ this.ID = id;
+ this.COUNTRYCODE = countryCode;
+ }
+
+ /**
+ * @return city id
+ * */
+ public int getId() {
+ return ID;
+ }
+
+ /**
+ * @return countrycode
+ * */
+ public String getCountryCode() {
+ return COUNTRYCODE;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ CityKey key = (CityKey)o;
+ return ID == key.ID &&
+ COUNTRYCODE.equals(key.COUNTRYCODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(ID, COUNTRYCODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return ID + " | " + COUNTRYCODE;
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
new file mode 100644
index 0000000..d9adee3
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.springdata.compoundkey;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.stereotype.Repository;
+
+/** City repository */
+@Repository
+@RepositoryConfig(cacheName = "City", autoCreateCache = true)
+public interface CityRepository extends IgniteRepository<City, CityKey> {
+ /**
+ * Find city by id
+ * @param id city identifier
+ * @return city
+ * */
+ public City findById(int id);
+
+ /**
+ * Find all cities by coutrycode
+ * @param cc coutrycode
+ * @return list of cache enrties CityKey -> City
+ * */
+ public List<Cache.Entry<CityKey, City>> findByCountryCode(String cc);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
new file mode 100644
index 0000000..03c05cd
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.springdata.compoundkey;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring application configuration
+ * */
+@Configuration
+@EnableIgniteRepositories
+public class CompoundKeyApplicationConfiguration {
+ /** */
+ public static final int CLI_CONN_PORT = 10810;
+
+ /**
+ * Ignite instance bean
+ * */
+ @Bean
+ public Ignite igniteInstance() {
+ return Ignition.start(new IgniteConfiguration()
+ .setClientConnectorConfiguration(new ClientConnectorConfiguration().setPort(CLI_CONN_PORT)));
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
new file mode 100644
index 0000000..bce3340
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.springdata.misc.SampleEvaluationContextExtension.SamplePassParamExtension;
+import org.apache.ignite.springdata20.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.repository.query.spi.EvaluationContextExtension;
+
+/** */
+@Configuration
+@EnableIgniteRepositories
+public class ApplicationConfiguration {
+ /** */
+ public static final String IGNITE_INSTANCE_ONE = "IGNITE_INSTANCE_ONE";
+
+ /** */
+ public static final String IGNITE_INSTANCE_TWO = "IGNITE_INSTANCE_TWO";
+
+ /**
+ * The bean with cache names
+ */
+ @Bean
+ public CacheNamesBean cacheNames() {
+ CacheNamesBean bean = new CacheNamesBean();
+
+ bean.setPersonCacheName("PersonCache");
+
+ return bean;
+ }
+
+ /** */
+ @Bean
+ public EvaluationContextExtension sampleSpELExtension() {
+ return new SampleEvaluationContextExtension();
+ }
+
+ /** */
+ @Bean(value = "sampleExtensionBean")
+ public SamplePassParamExtension sampleExtensionBean() {
+ return new SamplePassParamExtension();
+ }
+
+ /**
+ * Ignite instance bean - no instance name provided on RepositoryConfig
+ */
+ @Bean
+ public Ignite igniteInstance() {
+ IgniteConfiguration cfg = new IgniteConfiguration();
+
+ cfg.setIgniteInstanceName(IGNITE_INSTANCE_ONE);
+
+ CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+ ccfg.setIndexedTypes(Integer.class, Person.class);
+
+ cfg.setCacheConfiguration(ccfg);
+
+ TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+ spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+ cfg.setDiscoverySpi(spi);
+
+ return Ignition.start(cfg);
+ }
+
+ /**
+ * Ignite instance bean with not default name
+ */
+ @Bean
+ public Ignite igniteInstanceTWO() {
+ IgniteConfiguration cfg = new IgniteConfiguration();
+
+ cfg.setIgniteInstanceName(IGNITE_INSTANCE_TWO);
+
+ CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+ ccfg.setIndexedTypes(Integer.class, Person.class);
+
+ cfg.setCacheConfiguration(ccfg);
+
+ TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+ spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+ cfg.setDiscoverySpi(spi);
+
+ return Ignition.start(cfg);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
new file mode 100644
index 0000000..6185744
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
@@ -0,0 +1,42 @@
+/*
+ * 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.springdata.misc;
+
+/**
+ * The bean with cache names.
+ */
+public class CacheNamesBean {
+ /** */
+ private String personCacheName;
+
+ /**
+ * Get name of cache for persons.
+ *
+ * @return Name of cache.
+ */
+ public String getPersonCacheName() {
+ return personCacheName;
+ }
+
+ /**
+ * @param personCacheName Name of cache.
+ */
+ public void setPersonCacheName(String personCacheName) {
+ this.personCacheName = personCacheName;
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java
new file mode 100644
index 0000000..23f8f8e
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java
@@ -0,0 +1,33 @@
+/*
+ * 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.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface FullNameProjection {
+ /**
+ * Sample of using SpEL expression
+ * @return
+ */
+ @Value("#{target.firstName + ' ' + target.secondName}")
+ String getFullName();
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
new file mode 100644
index 0000000..531c67f
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
@@ -0,0 +1,100 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Objects;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * DTO class.
+ */
+public class Person {
+ /** First name. */
+ @QuerySqlField(index = true)
+ @QueryTextField
+ private String firstName;
+
+ /** Second name. */
+ @QuerySqlField(index = true)
+ private String secondName;
+
+ /**
+ * @param firstName First name.
+ * @param secondName Second name.
+ */
+ public Person(String firstName, String secondName) {
+ this.firstName = firstName;
+ this.secondName = secondName;
+ }
+
+ /**
+ * @return First name.
+ */
+ public String getFirstName() {
+ return firstName;
+ }
+
+ /**
+ * @param firstName First name.
+ */
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ /**
+ * @return Second name.
+ */
+ public String getSecondName() {
+ return secondName;
+ }
+
+ /**
+ * @param secondName Second name.
+ */
+ public void setSecondName(String secondName) {
+ this.secondName = secondName;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "Person{" +
+ "firstName='" + firstName + '\'' +
+ ", secondName='" + secondName + '\'' +
+ '}';
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ Person person = (Person)o;
+
+ return Objects.equals(firstName, person.firstName) &&
+ Objects.equals(secondName, person.secondName);
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(firstName, secondName);
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java
new file mode 100644
index 0000000..8222eb3
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java
@@ -0,0 +1,28 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+
+/**
+ *
+ */
+@RepositoryConfig(cacheName = "#{cacheNames.personCacheName}")
+public interface PersonExpressionRepository extends IgniteRepository<Person, Integer> {
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
new file mode 100644
index 0000000..2537c57
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
@@ -0,0 +1,54 @@
+/*
+ * 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.springdata.misc;
+
+import java.io.Serializable;
+
+/**
+ * Compound key.
+ */
+public class PersonKey implements Serializable {
+ /** */
+ private int id1;
+
+ /** */
+ private int id2;
+
+ /**
+ * @param id1 ID1.
+ * @param id2 ID2.
+ */
+ public PersonKey(int id1, int id2) {
+ this.id1 = id1;
+ this.id2 = id2;
+ }
+
+ /**
+ * @return ID1
+ */
+ public int getId1() {
+ return id1;
+ }
+
+ /**
+ * @return ID1
+ */
+ public int getId2() {
+ return id1;
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
new file mode 100644
index 0000000..a187a08
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface PersonProjection {
+ /** */
+ String getFirstName();
+
+ /**
+ * Sample of using registered spring bean into SpEL expression
+ * @return
+ */
+ @Value("#{@sampleExtensionBean.transformParam(target.firstName)}")
+ String getFirstNameTransformed();
+
+ /**
+ * Sample of using SpEL expression
+ * @return
+ */
+ @Value("#{target.firstName + ' ' + target.secondName}")
+ String getFullName();
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
new file mode 100644
index 0000000..c50a2d0
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -0,0 +1,148 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Integer> {
+ /** */
+ public List<Person> findByFirstName(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public <P> List<P> queryByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public <P> P queryOneByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+ /** */
+ @Query("firstName = ?#{[1]}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+ /** */
+ @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+ public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+ @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+ /** */
+ public List<Person> findByFirstNameContaining(String val);
+
+ /** */
+ public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+ /** */
+ public Collection<Person> findTopByFirstNameContaining(String val);
+
+ /** */
+ public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstName(String val);
+
+ /** */
+ public int countByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+ /** */
+ public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+ /** */
+ public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+ /** */
+ public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+ /** */
+ public PersonProjection findTopBySecondNameStartingWith(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<Person> simpleQuery(String val);
+
+ /** */
+ @Query("firstName REGEXP ?")
+ public List<Person> queryWithSort(String val, Sort sort);
+
+ /** */
+ @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+ public List<Person> queryWithPageable(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+ public List<String> selectField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public List<List> selectSeveralField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+ public int countQuery(String val);
+
+ /** Top 3 query */
+ public List<Person> findTop3ByFirstName(String val);
+
+ /** Delete query */
+ public long deleteByFirstName(String firstName);
+
+ /** Remove Query */
+ public List<Person> removeByFirstName(String firstName);
+
+ /** Delete using @Query with keyword in lower-case */
+ @Query("delete FROM Person WHERE secondName = ?")
+ public void deleteBySecondNameLowerCase(String secondName);
+
+ /** Delete using @Query but with errors on the query */
+ @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public void deleteWrongByFirstNameQuery(String firstName);
+
+ /** Update using @Query with keyword in mixed-case */
+ @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+ public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+ /** Update using @Query but with errors on the query */
+ @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
new file mode 100644
index 0000000..ac6231b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
@@ -0,0 +1,143 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ *
+ */
+@RepositoryConfig(igniteInstance = "igniteInstanceTWO", cacheName = "PersonCache")
+public interface PersonRepositoryOtherIgniteInstance extends IgniteRepository<Person, Integer> {
+ /** */
+ public List<Person> findByFirstName(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ /** */
+ @Query("firstName = ?#{[1]}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+ /** */
+ @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+ public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+ @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+ /** */
+ public List<Person> findByFirstNameContaining(String val);
+
+ /** */
+ public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+ /** */
+ public Collection<Person> findTopByFirstNameContaining(String val);
+
+ /** */
+ public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstName(String val);
+
+ /** */
+ public int countByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+ /** */
+ public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+ /** */
+ public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+ /** */
+ public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+ /** */
+ public PersonProjection findTopBySecondNameStartingWith(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<Person> simpleQuery(String val);
+
+ /** */
+ @Query("firstName REGEXP ?")
+ public List<Person> queryWithSort(String val, Sort sort);
+
+ /** */
+ @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+ public List<Person> queryWithPageable(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+ public List<String> selectField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public List<List> selectSeveralField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+ public int countQuery(String val);
+
+ /** Top 3 query */
+ public List<Person> findTop3ByFirstName(String val);
+
+ /** Delete query */
+ public long deleteByFirstName(String firstName);
+
+ /** Remove Query */
+ public List<Person> removeByFirstName(String firstName);
+
+ /** Delete using @Query with keyword in lower-case */
+ @Query("delete FROM Person WHERE secondName = ?")
+ public void deleteBySecondNameLowerCase(String secondName);
+
+ /** Delete using @Query but with errors on the query */
+ @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public void deleteWrongByFirstNameQuery(String firstName);
+
+ /** Update using @Query with keyword in mixed-case */
+ @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+ public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+ /** Update using @Query but with errors on the query */
+ @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
new file mode 100644
index 0000000..bf77597
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
@@ -0,0 +1,28 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonWithKeyCache", autoCreateCache = true)
+public interface PersonRepositoryWithCompoundKey extends IgniteRepository<Person, PersonKey> {
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
new file mode 100644
index 0000000..2bc713c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
@@ -0,0 +1,40 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.AbstractPageRequest;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonSecondRepository extends IgniteRepository<Person, Integer> {
+ /** */
+ public Slice<Cache.Entry<Integer, Person>> findBySecondNameIsNot(String val, PageRequest pageReq);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public Slice<List> querySliceOfList(String val, AbstractPageRequest pageReq);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
new file mode 100644
index 0000000..0fa535d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
@@ -0,0 +1,92 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport;
+
+/**
+ * Sample EvaluationContext Extension for Spring Data 2.0
+ * <p>
+ * Use SpEl expressions into your {@code @Query} definitions.
+ * <p>
+ * First, you need to register your extension into your spring data configuration. Sample:
+ * <pre>
+ * {@code @Configuration}
+ * {@code @EnableIgniteRepositories}(basePackages = ... )
+ * public class MyIgniteRepoConfig {
+ * ...
+ * {@code @Bean}
+ * public EvaluationContextExtension sampleSpELExtension() {
+ * return new SampleEvaluationContextExtension();
+ * }
+ * ...
+ * }
+ * </pre>
+ *
+ * <p>
+ * Sample of usage into your {@code @Query} definitions:
+ * <pre>
+ * {@code @RepositoryConfig}(cacheName = "users")
+ * public interface UserRepository
+ * extends IgniteRepository<User, UUID>{
+ * [...]
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = ?#{sampleExtension.transformParam(#email)}")
+ * User searchUserByEmail(@Param("email") String email);
+ *
+ * [...]
+ * }
+ * </pre>
+ * <p>
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class SampleEvaluationContextExtension extends EvaluationContextExtensionSupport {
+ /** */
+ private static final SamplePassParamExtension SAMPLE_PASS_PARAM_EXTENSION_INSTANCE = new SamplePassParamExtension();
+
+ /** */
+ private static final Map<String, Object> properties = new HashMap<>();
+
+ /** */
+ private static final String SAMPLE_EXTENSION_SPEL_VAR = "sampleExtension";
+
+ static {
+ properties.put(SAMPLE_EXTENSION_SPEL_VAR, SAMPLE_PASS_PARAM_EXTENSION_INSTANCE);
+ }
+
+ /** */
+ @Override public String getExtensionId() {
+ return "HK-SAMPLE-PASS-PARAM-EXTENSION";
+ }
+
+ /** */
+ @Override public Map<String, Object> getProperties() {
+ return properties;
+ }
+
+ /** */
+ public static class SamplePassParamExtension {
+ // just return same param
+ public Object transformParam(Object param) {
+ return param;
+ }
+ }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData2TestSuite.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData2TestSuite.java
new file mode 100644
index 0000000..4efc48b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData2TestSuite.java
@@ -0,0 +1,38 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.springdata.IgniteSpringDataCompoundKeyTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfExpressionTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfTest;
+import org.apache.ignite.springdata.IgniteSpringDataQueriesSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Spring Data 2.0 test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ IgniteSpringDataCrudSelfTest.class,
+ IgniteSpringDataQueriesSelfTest.class,
+ IgniteSpringDataCrudSelfExpressionTest.class,
+ IgniteSpringDataCompoundKeyTest.class
+})
+public class IgniteSpringData2TestSuite {
+}
diff --git a/modules/spring-data-2.2-ext/README.txt b/modules/spring-data-2.2-ext/README.txt
new file mode 100644
index 0000000..8489445
--- /dev/null
+++ b/modules/spring-data-2.2-ext/README.txt
@@ -0,0 +1,43 @@
+Apache Ignite Spring Module
+---------------------------
+
+Apache Ignite Spring Data 2.2 extension provides an integration with Spring Data 2.2 framework.
+
+Main features:
+
+- Supports multiple Ignite instances on same JVM (@RepositoryConfig).
+- Supports query tuning parameters in @Query annotation
+- Supports projections
+- Supports Page and Stream responses
+- Supports Sql Fields Query resultset transformation into the domain entity
+- Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam")
+- Supports advanced parameter binding and SpEL expressions into SQL queries:
+- Template variables:
+ - #entityName - the simple class name of the domain entity
+- Method parameter expressions: Parameters are exposed for indexed access ([0] is the first query method's param) or via the name declared using @Param. The actual SpEL expression binding is triggered by ?#. Example: ?#{[0] or ?#{#myParamName}
+- Advanced SpEL expressions: While advanced parameter binding is a very useful feature, the real power of SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components through SpEL EvaluationContext extension model.
+- Supports SpEL expressions into Text queries (TextQuery).
+
+Importing Spring Data 2.2 extension In Maven Project
+----------------------------------------
+
+If you are using Maven to manage dependencies of your project, you can add Spring Data 2.2 extension
+dependency like this (replace '${ignite-spring-data_2.2-ext.version}' with actual version of Ignite Spring Data 2.2
+extension you are interested in):
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ ...
+ <dependencies>
+ ...
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring-data_2.2-ext</artifactId>
+ <version>${ignite-spring-data_2.2-ext.version}</version>
+ </dependency>
+ ...
+ </dependencies>
+ ...
+</project>
diff --git a/modules/spring-data-2.2-ext/licenses/apache-2.0.txt b/modules/spring-data-2.2-ext/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/spring-data-2.2-ext/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/modules/spring-data-2.2-ext/modules/core/src/test/config/log4j-test.xml b/modules/spring-data-2.2-ext/modules/core/src/test/config/log4j-test.xml
new file mode 100755
index 0000000..3061bd4
--- /dev/null
+++ b/modules/spring-data-2.2-ext/modules/core/src/test/config/log4j-test.xml
@@ -0,0 +1,97 @@
+<?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.
+-->
+
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+ "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+<!--
+ Log4j configuration.
+-->
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+ <!--
+ Logs System.out messages to console.
+ -->
+ <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+ <!-- Log to STDOUT. -->
+ <param name="Target" value="System.out"/>
+
+ <!-- Log from DEBUG and higher. -->
+ <param name="Threshold" value="DEBUG"/>
+
+ <!-- The default pattern: Date Priority [Category] Message\n -->
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+
+ <!-- Do not log beyond INFO level. -->
+ <filter class="org.apache.log4j.varia.LevelRangeFilter">
+ <param name="levelMin" value="DEBUG"/>
+ <param name="levelMax" value="INFO"/>
+ </filter>
+ </appender>
+
+ <!--
+ Logs all System.err messages to console.
+ -->
+ <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+ <!-- Log to STDERR. -->
+ <param name="Target" value="System.err"/>
+
+ <!-- Log from WARN and higher. -->
+ <param name="Threshold" value="WARN"/>
+
+ <!-- The default pattern: Date Priority [Category] Message\n -->
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+ </appender>
+
+ <!--
+ Logs all output to specified file.
+ -->
+ <appender name="FILE" class="org.apache.log4j.RollingFileAppender">
+ <param name="Threshold" value="DEBUG"/>
+ <param name="File" value="ignite/work/log/ignite.log"/>
+ <param name="Append" value="true"/>
+ <param name="MaxFileSize" value="10MB"/>
+ <param name="MaxBackupIndex" value="10"/>
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+ </appender>
+
+ <!-- Disable all open source debugging. -->
+ <category name="org">
+ <level value="INFO"/>
+ </category>
+
+ <category name="org.eclipse.jetty">
+ <level value="INFO"/>
+ </category>
+
+ <!-- Default settings. -->
+ <root>
+ <!-- Print at info by default. -->
+ <level value="INFO"/>
+
+ <!-- Append to file and console. -->
+ <appender-ref ref="FILE"/>
+ <appender-ref ref="CONSOLE"/>
+ <appender-ref ref="CONSOLE_ERR"/>
+ </root>
+</log4j:configuration>
diff --git a/modules/spring-data-2.2-ext/modules/core/src/test/config/tests.properties b/modules/spring-data-2.2-ext/modules/core/src/test/config/tests.properties
new file mode 100644
index 0000000..0faf5b8
--- /dev/null
+++ b/modules/spring-data-2.2-ext/modules/core/src/test/config/tests.properties
@@ -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.
+#
+
+# Local address to bind to.
+local.ip=127.0.0.1
+
+# TCP communication port
+comm.tcp.port=30010
diff --git a/modules/spring-data-2.2-ext/pom.xml b/modules/spring-data-2.2-ext/pom.xml
new file mode 100644
index 0000000..b0c00ba
--- /dev/null
+++ b/modules/spring-data-2.2-ext/pom.xml
@@ -0,0 +1,154 @@
+<?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.
+-->
+
+<!--
+ POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-extensions-parent</artifactId>
+ <version>1</version>
+ <relativePath>../../parent</relativePath>
+ </parent>
+
+ <artifactId>ignite-spring-data_2.2-ext</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <url>http://ignite.apache.org</url>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-indexing</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-log4j</artifactId>
+ <version>${ignite.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.data</groupId>
+ <artifactId>spring-data-commons</artifactId>
+ <version>${spring.data-2.2.version}</version>
+ <!-- Exclude slf4j logging in favor of log4j -->
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring</artifactId>
+ <version>${ignite.version}</version>
+ <!--Remove exclusion while upgrading ignite-spring version to 5.2-->
+ <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>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-tools</artifactId>
+ <version>${ignite.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/IgniteRepository.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/IgniteRepository.java
new file mode 100644
index 0000000..a8c8c6f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/IgniteRepository.java
@@ -0,0 +1,109 @@
+/*
+ * 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.springdata22.repository;
+
+import java.io.Serializable;
+import java.util.Map;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Apache Ignite repository that extends basic capabilities of {@link CrudRepository}.
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public interface IgniteRepository<V, K extends Serializable> extends CrudRepository<V, K> {
+ /**
+ * Returns the Ignite instance bound to the repository
+ *
+ * @return the Ignite instance bound to the repository
+ */
+ public Ignite ignite();
+
+ /**
+ * Returns the Ignite Cache bound to the repository
+ *
+ * @return the Ignite Cache bound to the repository
+ */
+ public IgniteCache<K, V> cache();
+
+ /**
+ * Saves a given entity using provided key.
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Entity type.
+ * @param key Entity's key.
+ * @param entity Entity to save.
+ * @return Saved entity.
+ */
+ public <S extends V> S save(K key, S entity);
+
+ /**
+ * Saves all given keys and entities combinations.
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Type of entities.
+ * @param entities Map of key-entities pairs to save.
+ * @return Saved entities.
+ */
+ public <S extends V> Iterable<S> save(Map<K, S> entities);
+
+ /**
+ * Saves a given entity using provided key with expiry policy
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Entity type.
+ * @param key Entity's key.
+ * @param entity Entity to save.
+ * @param expiryPlc ExpiryPolicy to apply, if not null.
+ * @return Saved entity.
+ */
+ public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc);
+
+ /**
+ * Saves all given keys and entities combinations with expiry policy
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+ * (keys) that are not unique cluster wide.
+ *
+ * @param <S> Type of entities.
+ * @param entities Map of key-entities pairs to save.
+ * @param expiryPlc ExpiryPolicy to apply, if not null.
+ * @return Saved entities.
+ */
+ public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc);
+
+ /**
+ * Deletes all the entities for the provided ids.
+ *
+ * @param ids List of ids to delete.
+ */
+ public void deleteAllById(Iterable<K> ids);
+
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/DynamicQueryConfig.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/DynamicQueryConfig.java
new file mode 100644
index 0000000..f915267
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/DynamicQueryConfig.java
@@ -0,0 +1,348 @@
+/*
+ * 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.springdata22.repository.config;
+
+/**
+ * Runtime Dynamic query configuration.
+ * <p>
+ * Can be used as special repository method parameter to provide at runtime:
+ * <ol>
+ * <li>Dynamic query string (requires {@link Query#dynamicQuery()} == true)
+ * <li>Ignite query tuning*
+ * </ol>
+ * <p>
+ * * Please, note that {@link Query} annotation parameters will be ignored in favor of those defined in
+ * {@link DynamicQueryConfig} parameter if present.
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class DynamicQueryConfig {
+ /** */
+ private String value = "";
+
+ /** */
+ private boolean textQuery;
+
+ /** */
+ private boolean forceFieldsQry;
+
+ /** */
+ private boolean collocated;
+
+ /** */
+ private int timeout;
+
+ /** */
+ private boolean enforceJoinOrder;
+
+ /** */
+ private boolean distributedJoins;
+
+ /** */
+ private boolean lazy;
+
+ /** */
+ private boolean local;
+
+ /** */
+ private int[] parts;
+
+ /** */
+ private int limit;
+
+ /**
+ * From Query annotation.
+ *
+ * @param queryConfiguration the query configuration
+ * @return the dynamic query config
+ */
+ public static DynamicQueryConfig fromQueryAnnotation(Query queryConfiguration) {
+ DynamicQueryConfig config = new DynamicQueryConfig();
+ if (queryConfiguration != null) {
+ config.value = queryConfiguration.value();
+ config.collocated = queryConfiguration.collocated();
+ config.timeout = queryConfiguration.timeout();
+ config.enforceJoinOrder = queryConfiguration.enforceJoinOrder();
+ config.distributedJoins = queryConfiguration.distributedJoins();
+ config.lazy = queryConfiguration.lazy();
+ config.parts = queryConfiguration.parts();
+ config.local = queryConfiguration.local();
+ config.limit = queryConfiguration.limit();
+ }
+ return config;
+ }
+
+ /**
+ * Query text string.
+ *
+ * @return the string
+ */
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Whether must use TextQuery search.
+ *
+ * @return the boolean
+ */
+ public boolean textQuery() {
+ return textQuery;
+ }
+
+ /**
+ * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+ * statements or to not return hidden fields on SELECT * statements.
+ *
+ * @return the boolean
+ */
+ public boolean forceFieldsQuery() {
+ return forceFieldsQry;
+ }
+
+ /**
+ * Sets flag defining if this query is collocated.
+ * <p>
+ * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+ * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+ * of your query selection are collocated together on the same node and you group by collocated key (primary or
+ * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+ * nodes.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ *
+ * @return the boolean
+ */
+ public boolean collocated() {
+ return collocated;
+ }
+
+ /**
+ * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+ * timeout is exceeded. Zero value disables timeout
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ *
+ * @return the int
+ */
+ public int timeout() {
+ return timeout;
+ }
+
+ /**
+ * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+ * tables in join. By default is {@code false}.
+ * <p>
+ * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+ * correct and tuned as much as possible but query optimizer still produces wrong join order.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ *
+ * @return the boolean
+ */
+ public boolean enforceJoinOrder() {
+ return enforceJoinOrder;
+ }
+
+ /**
+ * Specify if distributed joins are enabled for this query.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ *
+ * @return the boolean
+ */
+ public boolean distributedJoins() {
+ return distributedJoins;
+ }
+
+ /**
+ * Sets lazy query execution flag.
+ * <p>
+ * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+ * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+ * increasing concurrency.
+ * <p>
+ * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+ * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+ * consumption at the cost of moderate performance hit.
+ * <p>
+ * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ *
+ * @return the boolean
+ */
+ public boolean lazy() {
+ return lazy;
+ }
+
+ /**
+ * Sets whether this query should be executed on local node only.
+ *
+ * @return the boolean
+ */
+ public boolean local() {
+ return local;
+ }
+
+ /**
+ * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+ * partitions.
+ * <p>
+ * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ *
+ * @return the int [ ]
+ */
+ public int[] parts() {
+ return parts;
+ }
+
+ /**
+ * Gets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+ *
+ * @return Limit value.
+ */
+ public int limit() {
+ return limit;
+ }
+
+ /**
+ * Sets value.
+ *
+ * @param value the value
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setValue(String value) {
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * Sets text query.
+ *
+ * @param textQuery the text query
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setTextQuery(boolean textQuery) {
+ this.textQuery = textQuery;
+ return this;
+ }
+
+ /**
+ * Sets force fields query.
+ *
+ * @param forceFieldsQuery the force fields query
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setForceFieldsQuery(boolean forceFieldsQuery) {
+ forceFieldsQry = forceFieldsQuery;
+ return this;
+ }
+
+ /**
+ * Sets collocated.
+ *
+ * @param collocated the collocated
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setCollocated(boolean collocated) {
+ this.collocated = collocated;
+ return this;
+ }
+
+ /**
+ * Sets timeout.
+ *
+ * @param timeout the timeout
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setTimeout(int timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ /**
+ * Sets enforce join order.
+ *
+ * @param enforceJoinOrder the enforce join order
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setEnforceJoinOrder(boolean enforceJoinOrder) {
+ this.enforceJoinOrder = enforceJoinOrder;
+ return this;
+ }
+
+ /**
+ * Sets distributed joins.
+ *
+ * @param distributedJoins the distributed joins
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setDistributedJoins(boolean distributedJoins) {
+ this.distributedJoins = distributedJoins;
+ return this;
+ }
+
+ /**
+ * Sets lazy.
+ *
+ * @param lazy the lazy
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setLazy(boolean lazy) {
+ this.lazy = lazy;
+ return this;
+ }
+
+ /**
+ * Sets local.
+ *
+ * @param local the local
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setLocal(boolean local) {
+ this.local = local;
+ return this;
+ }
+
+ /**
+ * Sets parts.
+ *
+ * @param parts the parts
+ * @return this for chaining
+ */
+ public DynamicQueryConfig setParts(int[] parts) {
+ this.parts = parts;
+ return this;
+ }
+
+ /**
+ * Sets limit to response records count for TextQuery.
+ *
+ * @param limit If 0 or less, considered to be no limit.
+ * @return {@code this} For chaining.
+ */
+ public DynamicQueryConfig setLimit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/EnableIgniteRepositories.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/EnableIgniteRepositories.java
new file mode 100644
index 0000000..ad465c3
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/EnableIgniteRepositories.java
@@ -0,0 +1,120 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.ignite.springdata22.repository.support.IgniteRepositoryFactoryBean;
+import org.apache.ignite.springdata22.repository.support.IgniteRepositoryImpl;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate Apache Ignite repositories. If no base package is configured through either {@link #value()},
+ * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(IgniteRepositoriesRegistar.class)
+public @interface EnableIgniteRepositories {
+ /**
+ * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
+ * {@code @EnableIgniteRepositories("org.my.pkg")} instead of
+ * {@code @EnableIgniteRepositories(basePackages="org.my.pkg")}.
+ */
+ String[] value() default {};
+
+ /**
+ * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with)
+ * this attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+ */
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components.
+ * The package of each class specified will be scanned. Consider creating a special no-op marker class or interface
+ * in each package that serves no purpose other than being referenced by this attribute.
+ */
+ Class<?>[] basePackageClasses() default {};
+
+ /**
+ * Specifies which types are not eligible for component scanning.
+ */
+ Filter[] excludeFilters() default {};
+
+ /**
+ * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+ * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or
+ * filters.
+ */
+ Filter[] includeFilters() default {};
+
+ /**
+ * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+ * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+ * for {@code PersonRepositoryImpl}.
+ *
+ * @return Postfix to be used when looking up custom repository implementations.
+ */
+ String repositoryImplementationPostfix() default "Impl";
+
+ /**
+ * Configures the location of where to find the Spring Data named queries properties file.
+ *
+ * @return Location of where to find the Spring Data named queries properties file.
+ */
+ String namedQueriesLocation() default "";
+
+ /**
+ * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+ * {@link Key#CREATE_IF_NOT_FOUND}.
+ *
+ * @return Key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods.
+ */
+ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+ /**
+ * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to {@link
+ * IgniteRepositoryFactoryBean}.
+ *
+ * @return {@link FactoryBean} class to be used for each repository instance.
+ */
+ Class<?> repositoryFactoryBeanClass() default IgniteRepositoryFactoryBean.class;
+
+ /**
+ * Configure the repository base class to be used to create repository proxies for this particular configuration.
+ *
+ * @return Repository base class to be used to create repository proxies for this particular configuration.
+ */
+ Class<?> repositoryBaseClass() default IgniteRepositoryImpl.class;
+
+ /**
+ * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+ * repositories infrastructure.
+ */
+ boolean considerNestedRepositories() default false;
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoriesRegistar.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoriesRegistar.java
new file mode 100644
index 0000000..ddecd77
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoriesRegistar.java
@@ -0,0 +1,37 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryBeanDefinitionRegistrarSupport}.
+ */
+public class IgniteRepositoriesRegistar extends RepositoryBeanDefinitionRegistrarSupport {
+ /** {@inheritDoc} */
+ @Override protected Class<? extends Annotation> getAnnotation() {
+ return EnableIgniteRepositories.class;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryConfigurationExtension getExtension() {
+ return new IgniteRepositoryConfigurationExtension();
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoryConfigurationExtension.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoryConfigurationExtension.java
new file mode 100644
index 0000000..4989196
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoryConfigurationExtension.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryConfigurationExtension}.
+ */
+public class IgniteRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
+ /** {@inheritDoc} */
+ @Override public String getModuleName() {
+ return "Apache Ignite";
+ }
+
+ /** {@inheritDoc} */
+ @Override protected String getModulePrefix() {
+ return "ignite";
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getRepositoryFactoryBeanClassName() {
+ return IgniteRepositoryFactoryBean.class.getName();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Collection<Class<?>> getIdentifyingTypes() {
+ return Collections.singleton(IgniteRepository.class);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/Query.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/Query.java
new file mode 100644
index 0000000..d1af467
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/Query.java
@@ -0,0 +1,136 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to provide a user defined query for a method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Query {
+ /**
+ * Query text string. If not provided, Ignite query generator for Spring Data framework will be used to generate one
+ * (only if textQuery = false (default))
+ */
+ String value() default "";
+
+ /**
+ * Whether annotated repository method must use TextQuery search.
+ */
+ boolean textQuery() default false;
+
+ /**
+ * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+ * statements or to not return hidden fields on SELECT * statements.
+ */
+ boolean forceFieldsQuery() default false;
+
+ /**
+ * Sets flag defining if this query is collocated.
+ * <p>
+ * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+ * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+ * of your query selection are collocated together on the same node and you group by collocated key (primary or
+ * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+ * nodes.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ */
+ boolean collocated() default false;
+
+ /**
+ * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+ * timeout is exceeded. Zero value disables timeout
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ */
+ int timeout() default 0;
+
+ /**
+ * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+ * tables in join. By default is {@code false}.
+ * <p>
+ * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+ * correct and tuned as much as possible but query optimizer still produces wrong join order.
+ *
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ */
+ boolean enforceJoinOrder() default false;
+
+ /**
+ * Specify if distributed joins are enabled for this query.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ */
+ boolean distributedJoins() default false;
+
+ /**
+ * Sets lazy query execution flag.
+ * <p>
+ * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+ * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+ * increasing concurrency.
+ * <p>
+ * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+ * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+ * consumption at the cost of moderate performance hit.
+ * <p>
+ * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+ * <p>
+ * Only applicable to SqlFieldsQuery
+ */
+ boolean lazy() default false;
+
+ /**
+ * Sets whether this query should be executed on local node only.
+ */
+ boolean local() default false;
+
+ /**
+ * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+ * partitions.
+ * <p>
+ * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+ * <p>
+ * Only applicable to SqlFieldsQuery and SqlQuery
+ */
+ int[] parts() default {};
+
+ /**
+ * Specify whether the annotated method must provide a non null {@link DynamicQueryConfig} parameter with a non
+ * empty value (query string) or {@link DynamicQueryConfig#textQuery()} == true.
+ * <p>
+ * Please, note that {@link DynamicQueryConfig#textQuery()} annotation parameters will be ignored in favor of those
+ * defined in {@link DynamicQueryConfig} parameter if present (runtime ignite query tuning).
+ */
+ boolean dynamicQuery() default false;
+
+ /**
+ * Sets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+ */
+ int limit() default 0;
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/RepositoryConfig.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/RepositoryConfig.java
new file mode 100644
index 0000000..6699ed6
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/RepositoryConfig.java
@@ -0,0 +1,77 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+
+/**
+ * The annotation can be used to pass Ignite specific parameters to a bound repository.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepositoryConfig {
+ /**
+ * Cache name string.
+ *
+ * @return A name of a distributed Apache Ignite cache an annotated repository will be mapped to.
+ */
+ String cacheName() default "";
+
+ /**
+ * Ignite instance string. Default "igniteInstance".
+ *
+ * @return {@link Ignite} instance spring bean name
+ */
+ String igniteInstance() default "igniteInstance";
+
+ /**
+ * Ignite cfg string. Default "igniteCfg".
+ *
+ * @return {@link IgniteConfiguration} spring bean name
+ */
+ String igniteCfg() default "igniteCfg";
+
+ /**
+ * Ignite spring cfg path string. Default "igniteSpringCfgPath".
+ *
+ * @return A path to Ignite's Spring XML configuration spring bean name
+ */
+ String igniteSpringCfgPath() default "igniteSpringCfgPath";
+
+ /**
+ * Auto create cache. Default false to enforce control over cache creation and to avoid cache creation by mistake
+ * <p>
+ * Tells to Ignite Repository factory wether cache should be auto created if not exists.
+ *
+ * @return the boolean
+ */
+ boolean autoCreateCache() default false;
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/package-info.java
new file mode 100644
index 0000000..4e3203b
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/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. -->
+ * Package includes Spring Data integration related configuration files.
+ */
+package org.apache.ignite.springdata22.repository.config;
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/package-info.java
new file mode 100644
index 0000000..2a865f5
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/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. -->
+ * Package contains Apache Ignite Spring Data integration.
+ */
+package org.apache.ignite.springdata22.repository;
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/DeclaredQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/DeclaredQuery.java
new file mode 100644
index 0000000..74ab2cc
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/DeclaredQuery.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed 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.springdata22.repository.query;
+
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.StringUtils;
+
+/**
+ * A wrapper for a String representation of a query offering information about the query.
+ *
+ * @author Jens Schauder
+ */
+interface DeclaredQuery {
+ /**
+ * Creates a {@literal DeclaredQuery} from a query {@literal String}.
+ *
+ * @param qry might be {@literal null} or empty.
+ * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument.
+ */
+ public static DeclaredQuery of(@Nullable String qry) {
+ return StringUtils.isEmpty(qry) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(qry);
+ }
+
+ /**
+ * @return whether the underlying query has at least one named parameter.
+ */
+ public boolean hasNamedParameter();
+
+ /**
+ * Returns the query string.
+ */
+ public String getQueryString();
+
+ /**
+ * Returns the main alias used in the query.
+ *
+ * @return the alias
+ */
+ @Nullable
+ public String getAlias();
+
+ /**
+ * Returns whether the query is using a constructor expression.
+ */
+ public boolean hasConstructorExpression();
+
+ /**
+ * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
+ */
+ public boolean isDefaultProjection();
+
+ /**
+ * Returns the {@link StringQuery.ParameterBinding}s registered.
+ */
+ public List<StringQuery.ParameterBinding> getParameterBindings();
+
+ /**
+ * Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to
+ * be expected from the original query, either derived from the query wrapped by this instance or from the
+ * information passed as arguments.
+ *
+ * @param cntQry an optional query string to be used if present.
+ * @param cntQryProjection an optional return type for the query.
+ * @return a new {@literal DeclaredQuery} instance.
+ */
+ public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection);
+
+ /**
+ * @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
+ */
+ public default boolean usesPaging() {
+ return false;
+ }
+
+ /**
+ * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
+ * name.
+ *
+ * @return Whether the query uses JDBC style parameters.
+ */
+ public boolean usesJdbcStyleParameters();
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/EmptyDeclaredQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/EmptyDeclaredQuery.java
new file mode 100644
index 0000000..7803537
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/EmptyDeclaredQuery.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed 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.springdata22.repository.query;
+
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * NULL-Object pattern implementation for {@link DeclaredQuery}.
+ *
+ * @author Jens Schauder
+ */
+class EmptyDeclaredQuery implements DeclaredQuery {
+ /**
+ * An implementation implementing the NULL-Object pattern for situations where there is no query.
+ */
+ static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery();
+
+ /** {@inheritDoc} */
+ @Override public boolean hasNamedParameter() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getQueryString() {
+ return "";
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getAlias() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean hasConstructorExpression() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean isDefaultProjection() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<StringQuery.ParameterBinding> getParameterBindings() {
+ return Collections.emptyList();
+ }
+
+ /** {@inheritDoc} */
+ @Override public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection) {
+ Assert.hasText(cntQry, "CountQuery must not be empty!");
+ return DeclaredQuery.of(cntQry);
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean usesJdbcStyleParameters() {
+ return false;
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/ExpressionBasedStringQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/ExpressionBasedStringQuery.java
new file mode 100644
index 0000000..bdbd81d
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/ExpressionBasedStringQuery.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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.springdata22.repository.query;
+
+import java.util.regex.Pattern;
+import org.springframework.data.repository.core.EntityMetadata;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.util.Assert;
+
+/**
+ * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression.
+ * <p>
+ * Currently the following template variables are available:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the given entity</li>
+ * <ol>
+ *
+ * @author Thomas Darimont
+ * @author Oliver Gierke
+ * @author Tom Hombergs
+ */
+class ExpressionBasedStringQuery extends StringQuery {
+ /**
+ * Expression parameter.
+ */
+ private static final String EXPRESSION_PARAMETER = "?#{";
+
+ /**
+ * Quoted expression parameter.
+ */
+ private static final String QUOTED_EXPRESSION_PARAMETER = "?__HASH__{";
+
+ /**
+ * Expression parameter quoting.
+ */
+ private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile(Pattern.quote(EXPRESSION_PARAMETER));
+
+ /**
+ * Expression parameter unquoting.
+ */
+ private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile(
+ Pattern.quote(QUOTED_EXPRESSION_PARAMETER));
+
+ /**
+ * Entity name.
+ */
+ private static final String ENTITY_NAME = "entityName";
+
+ /**
+ * Entity name variable.
+ */
+ private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME;
+
+ /**
+ * Entity name variable expression.
+ */
+ private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}";
+
+ /**
+ * Creates a new instance for the given query and {@link EntityMetadata}.
+ *
+ * @param qry must not be {@literal null} or empty.
+ * @param metadata must not be {@literal null}.
+ * @param parser must not be {@literal null}.
+ */
+ public ExpressionBasedStringQuery(String qry, RepositoryMetadata metadata, SpelExpressionParser parser) {
+ super(renderQueryIfExpressionOrReturnQuery(qry, metadata, parser));
+ }
+
+ /**
+ * Creates an instance from a given {@link DeclaredQuery}.
+ *
+ * @param qry the original query. Must not be {@literal null}.
+ * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+ * @param parser Parser for resolving SpEL expressions. Must not be {@literal null}.
+ * @return A query supporting SpEL expressions.
+ */
+ static ExpressionBasedStringQuery from(DeclaredQuery qry,
+ RepositoryMetadata metadata,
+ SpelExpressionParser parser) {
+ return new ExpressionBasedStringQuery(qry.getQueryString(), metadata, parser);
+ }
+
+ /**
+ * @param qry, the query expression potentially containing a SpEL expression. Must not be {@literal null}.}
+ * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+ * @param parser Must not be {@literal null}.
+ * @return rendered query
+ */
+ private static String renderQueryIfExpressionOrReturnQuery(String qry,
+ RepositoryMetadata metadata,
+ SpelExpressionParser parser) {
+
+ Assert.notNull(qry, "query must not be null!");
+ Assert.notNull(metadata, "metadata must not be null!");
+ Assert.notNull(parser, "parser must not be null!");
+
+ if (!containsExpression(qry))
+ return qry;
+
+ StandardEvaluationContext evalCtx = new StandardEvaluationContext();
+ evalCtx.setVariable(ENTITY_NAME, metadata.getDomainType().getSimpleName());
+
+ qry = potentiallyQuoteExpressionsParameter(qry);
+
+ Expression expr = parser.parseExpression(qry, ParserContext.TEMPLATE_EXPRESSION);
+
+ String result = expr.getValue(evalCtx, String.class);
+
+ if (result == null)
+ return qry;
+
+ return potentiallyUnquoteParameterExpressions(result);
+ }
+
+ /**
+ * @param result Result.
+ */
+ private static String potentiallyUnquoteParameterExpressions(String result) {
+ return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER);
+ }
+
+ /**
+ * @param qry Query.
+ */
+ private static String potentiallyQuoteExpressionsParameter(String qry) {
+ return EXPRESSION_PARAMETER_QUOTING.matcher(qry).replaceAll(QUOTED_EXPRESSION_PARAMETER);
+ }
+
+ /**
+ * @param qry Query.
+ */
+ private static boolean containsExpression(String qry) {
+ return qry.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQuery.java
new file mode 100644
index 0000000..b045a4f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQuery.java
@@ -0,0 +1,134 @@
+/*
+ * 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.springdata22.repository.query;
+
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Ignite query helper class. For internal use only.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteQuery {
+ /** */
+ enum Option {
+ /** Query will be used with Sort object. */
+ SORTING,
+
+ /** Query will be used with Pageable object. */
+ PAGINATION,
+
+ /** No advanced option. */
+ NONE
+ }
+
+ /**
+ * Query text string.
+ */
+ private final String qrySql;
+
+ /**
+ * Whether this is a SQL fields query
+ */
+ private final boolean isFieldQuery;
+
+ /**
+ * Whether this is Text query
+ */
+ private final boolean isTextQuery;
+
+ /**
+ * Whether was autogenerated (by method name)
+ */
+ private final boolean isAutogenerated;
+
+ /**
+ * Type of option.
+ */
+ private final Option option;
+
+ /**
+ * @param qrySql the query string.
+ * @param isFieldQuery Is field query.
+ * @param isTextQuery Is a TextQuery
+ * @param isAutogenerated query was autogenerated
+ * @param option Option.
+ */
+ public IgniteQuery(String qrySql,
+ boolean isFieldQuery,
+ boolean isTextQuery,
+ boolean isAutogenerated,
+ Option option) {
+ this.qrySql = qrySql;
+ this.isFieldQuery = isFieldQuery;
+ this.isTextQuery = isTextQuery;
+ this.isAutogenerated = isAutogenerated;
+ this.option = option;
+ }
+
+ /**
+ * Text string of the query.
+ *
+ * @return SQL query text string.
+ */
+ public String qryStr() {
+ return qrySql;
+ }
+
+ /**
+ * Returns {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+ *
+ * @return {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+ */
+ public boolean isFieldQuery() {
+ return isFieldQuery;
+ }
+
+ /**
+ * Returns {@code true} if it's Ignite Text query, {@code false} otherwise.
+ *
+ * @return {@code true} if it's Ignite Text query, {@code false} otherwise.
+ */
+ public boolean isTextQuery() {
+ return isTextQuery;
+ }
+
+ /**
+ * Returns {@code true} if it's autogenerated, {@code false} otherwise.
+ *
+ * @return {@code true} if it's autogenerated, {@code false} otherwise.
+ */
+ public boolean isAutogenerated() {
+ return isAutogenerated;
+ }
+
+ /**
+ * Advanced querying option.
+ *
+ * @return querying option.
+ */
+ public Option options() {
+ return option;
+ }
+
+ /** */
+ @Override public String toString() {
+ return S.toString(IgniteQuery.class, this);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQueryGenerator.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQueryGenerator.java
new file mode 100644
index 0000000..a0be11d
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQueryGenerator.java
@@ -0,0 +1,276 @@
+/*
+ * 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.springdata22.repository.query;
+
+import java.lang.reflect.Method;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * Ignite query generator for Spring Data framework.
+ */
+public class IgniteQueryGenerator {
+ /** */
+ private IgniteQueryGenerator() {
+ // No-op.
+ }
+
+ /**
+ * @param mtd Method.
+ * @param metadata Metadata.
+ * @return Generated ignite query.
+ */
+ public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
+ PartTree parts;
+
+ try {
+ parts = new PartTree(mtd.getName(), metadata.getDomainType());
+ }
+ catch (PropertyReferenceException e) {
+ parts = new PartTree(mtd.getName(), metadata.getIdType());
+ }
+
+ boolean isCountOrFieldQuery = parts.isCountProjection();
+
+ StringBuilder sql = new StringBuilder();
+
+ if (parts.isDelete()) {
+ sql.append("DELETE ");
+
+ // For the DML queries aside from SELECT *, they should run over SqlFieldQuery
+ isCountOrFieldQuery = true;
+ }
+ else {
+ sql.append("SELECT ");
+
+ if (parts.isDistinct())
+ throw new UnsupportedOperationException("DISTINCT clause in not supported.");
+
+ if (isCountOrFieldQuery)
+ sql.append("COUNT(1) ");
+ else
+ sql.append("* ");
+ }
+
+ sql.append("FROM ").append(metadata.getDomainType().getSimpleName());
+
+ if (parts.iterator().hasNext()) {
+ sql.append(" WHERE ");
+
+ for (PartTree.OrPart orPart : parts) {
+ sql.append("(");
+
+ for (Part part : orPart) {
+ handleQueryPart(sql, part, metadata.getDomainType());
+ sql.append(" AND ");
+ }
+
+ sql.delete(sql.length() - 5, sql.length());
+
+ sql.append(") OR ");
+ }
+
+ sql.delete(sql.length() - 4, sql.length());
+ }
+
+ addSorting(sql, parts.getSort());
+
+ if (parts.isLimiting()) {
+ sql.append(" LIMIT ");
+ sql.append(parts.getMaxResults().intValue());
+ }
+
+ return new IgniteQuery(sql.toString(), isCountOrFieldQuery, false, true, getOptions(mtd));
+ }
+
+ /**
+ * Add a dynamic part of query for the sorting support.
+ *
+ * @param sql SQL text string.
+ * @param sort Sort method.
+ * @return Sorting criteria in StringBuilder.
+ */
+ public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
+ if (sort != null && sort != Sort.unsorted()) {
+ sql.append(" ORDER BY ");
+
+ for (Sort.Order order : sort) {
+ sql.append(order.getProperty()).append(" ").append(order.getDirection());
+
+ if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
+ sql.append(" ").append("NULL ");
+
+ switch (order.getNullHandling()) {
+ case NULLS_FIRST:
+ sql.append("FIRST");
+ break;
+ case NULLS_LAST:
+ sql.append("LAST");
+ break;
+ default:
+ }
+ }
+ sql.append(", ");
+ }
+
+ sql.delete(sql.length() - 2, sql.length());
+ }
+
+ return sql;
+ }
+
+ /**
+ * Add a dynamic part of a query for the pagination support.
+ *
+ * @param sql Builder instance.
+ * @param pageable Pageable instance.
+ * @return Builder instance.
+ */
+ public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {
+
+ addSorting(sql, pageable.getSort());
+
+ sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());
+
+ return sql;
+ }
+
+ /**
+ * Determines whether query is dynamic or not (by list of method parameters)
+ *
+ * @param mtd Method.
+ * @return type of options
+ */
+ public static IgniteQuery.Option getOptions(Method mtd) {
+ IgniteQuery.Option option = IgniteQuery.Option.NONE;
+
+ Class<?>[] types = mtd.getParameterTypes();
+ if (types.length > 0) {
+ Class<?> type = types[types.length - 1];
+
+ if (Sort.class.isAssignableFrom(type))
+ option = IgniteQuery.Option.SORTING;
+ else if (Pageable.class.isAssignableFrom(type))
+ option = IgniteQuery.Option.PAGINATION;
+ }
+
+ for (int i = 0; i < types.length - 1; i++) {
+ Class<?> tp = types[i];
+
+ if (tp == Sort.class || tp == Pageable.class)
+ throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
+ }
+
+ return option;
+ }
+
+ /**
+ * Check and correct table name if using column name from compound key.
+ */
+ private static String getColumnName(Part part, Class<?> domainType) {
+ PropertyPath prperty = part.getProperty();
+
+ if (prperty.getType() != domainType)
+ return domainType.getSimpleName() + "." + prperty.getSegment();
+ else
+ return part.toString();
+ }
+
+ /**
+ * Transform part to qryStr expression
+ */
+ private static void handleQueryPart(StringBuilder sql, Part part, Class<?> domainType) {
+ sql.append("(");
+
+ sql.append(getColumnName(part, domainType));
+
+ switch (part.getType()) {
+ case SIMPLE_PROPERTY:
+ sql.append("=?");
+ break;
+ case NEGATING_SIMPLE_PROPERTY:
+ sql.append("<>?");
+ break;
+ case GREATER_THAN:
+ sql.append(">?");
+ break;
+ case GREATER_THAN_EQUAL:
+ sql.append(">=?");
+ break;
+ case LESS_THAN:
+ sql.append("<?");
+ break;
+ case LESS_THAN_EQUAL:
+ sql.append("<=?");
+ break;
+ case IS_NOT_NULL:
+ sql.append(" IS NOT NULL");
+ break;
+ case IS_NULL:
+ sql.append(" IS NULL");
+ break;
+ case BETWEEN:
+ sql.append(" BETWEEN ? AND ?");
+ break;
+ case FALSE:
+ sql.append(" = FALSE");
+ break;
+ case TRUE:
+ sql.append(" = TRUE");
+ break;
+ //TODO: review this legacy code, LIKE should be -> LIKE ?
+ case LIKE:
+ case CONTAINING:
+ sql.append(" LIKE '%' || ? || '%'");
+ break;
+ case NOT_CONTAINING:
+ //TODO: review this legacy code, NOT_LIKE should be -> NOT LIKE ?
+ case NOT_LIKE:
+ sql.append(" NOT LIKE '%' || ? || '%'");
+ break;
+ case STARTING_WITH:
+ sql.append(" LIKE ? || '%'");
+ break;
+ case ENDING_WITH:
+ sql.append(" LIKE '%' || ?");
+ break;
+ case IN:
+ sql.append(" IN ?");
+ break;
+ case NOT_IN:
+ sql.append(" NOT IN ?");
+ break;
+ case REGEX:
+ sql.append(" REGEXP ?");
+ break;
+ case NEAR:
+ case AFTER:
+ case BEFORE:
+ case EXISTS:
+ default:
+ throw new UnsupportedOperationException(part.getType() + " is not supported!");
+ }
+
+ sql.append(")");
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteRepositoryQuery.java
new file mode 100644
index 0000000..9ab9606
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteRepositoryQuery.java
@@ -0,0 +1,1043 @@
+/*
+ * 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.springdata22.repository.query;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.AbstractCollection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.cache.Cache;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.cache.query.TextQuery;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryUtils;
+import org.apache.ignite.internal.processors.cache.CacheEntryImpl;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.cache.binary.IgniteBinaryImpl;
+import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.springdata22.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata22.repository.query.StringQuery.ParameterBinding;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.Parameter;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.StringUtils;
+
+import static org.apache.ignite.springdata22.repository.support.IgniteRepositoryFactory.isFieldQuery;
+
+/**
+ * Ignite query implementation.
+ * <p>
+ * <p>
+ * Features:
+ * <ol>
+ * <li> Supports query tuning parameters</li>
+ * <li> Supports projections</li>
+ * <li> Supports Page and Stream responses</li>
+ * <li> Supports SqlFieldsQuery resultset transformation into the domain entity</li>
+ * <li> Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam") annotation</li>
+ * <li> Supports advanced parameter binding and SpEL expressions into SQL queries
+ * <ol>
+ * <li><b>Template variables</b>:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the domain entity</li>
+ * </ol>
+ * </li>
+ * <li><b>Method parameter expressions</b>: Parameters are exposed for indexed access ([0] is the first query method's
+ * param) or via the name declared using @Param. The actual SpEL expression binding is triggered by '?#'. Example:
+ * ?#{[0]} or ?#{#myParamName}</li>
+ * <li><b>Advanced SpEL expressions</b>: While advanced parameter binding is a very useful feature, the real power of
+ * SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components
+ * through SpEL EvaluationContext extension model.</li>
+ * </ol>
+ * Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where country = ?#{[0]} and city = ?#{[1]}")
+ * List<User> searchUsersByCity({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * Pageable pageable);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = ?")
+ * User searchUserByEmail(String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where lucene = ?#{
+ * luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder.match('city',#city)).build()}")
+ * List<User> searchUsersByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports SpEL expressions into Text queries ({@link TextQuery}). Examples:
+ * <pre>
+ * {@code @Query}(textQuery = true, value = "email: #{#email}")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(textQuery = true, value = "#{#textToSearch}")
+ * List<User> searchUsersByText({@code @Param}("textToSearch") String text, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{[0]}")
+ * List<User> searchUsersByText(String textToSearch, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder
+ * .match('city', #city)).build()}")
+ * List<User> searchUserByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports dynamic query and tuning at runtime by using {@link DynamicQueryConfig} method parameter. Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmailWithQueryTuning({@code @Param}("email") String email, {@code @Param}("ignoredUsedAsQueryTuning") DynamicQueryConfig config);
+ *
+ * {@code @Query}(dynamicQuery = true)
+ * List<User> searchUsersByCityWithDynamicQuery({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * {@code @Param}("ignoredUsedAsDynamicQueryAndTuning") DynamicQueryConfig config, Pageable pageable);
+ *
+ * ...
+ * DynamicQueryConfig onlyTunning = new DynamicQueryConfig().setCollocated(true);
+ * repo.searchUserByEmailWithQueryTuning("user@mail.com", onlyTunning);
+ *
+ * DynamicQueryConfig withDynamicQuery = new DynamicQueryConfig().value("SELECT * from #{#entityName} where country = ?#{[0] and city = ?#{[1]}").setForceFieldsQuery(true).setLazy(true).setCollocated(true);
+ * repo.searchUsersByCityWithDynamicQuery("Spain", "Madrid", withDynamicQuery, new PageRequest(0, 100));
+ *
+ * </pre>
+ * </li>
+ * </ol>
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@SuppressWarnings("unchecked")
+public class IgniteRepositoryQuery implements RepositoryQuery {
+ /** */
+ private static final TreeMap<String, Class<?>> binaryFieldClass = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ /**
+ * Defines the way how to process query result
+ */
+ private enum ReturnStrategy {
+ /** Need to return only one value. */
+ ONE_VALUE,
+
+ /** Need to return one cache entry */
+ CACHE_ENTRY,
+
+ /** Need to return list of cache entries */
+ LIST_OF_CACHE_ENTRIES,
+
+ /** Need to return list of values */
+ LIST_OF_VALUES,
+
+ /** Need to return list of lists */
+ LIST_OF_LISTS,
+
+ /** Need to return slice */
+ SLICE_OF_VALUES,
+
+ /** Slice of cache entries */
+ SLICE_OF_CACHE_ENTRIES,
+
+ /** Slice of lists */
+ SLICE_OF_LISTS,
+
+ /** Need to return Page of values */
+ PAGE_OF_VALUES,
+
+ /** Need to return stream of values */
+ STREAM_OF_VALUES,
+ }
+
+ /** */
+ private final Class<?> type;
+
+ /** */
+ private final IgniteQuery staticQuery;
+
+ /** */
+ private final IgniteCache cache;
+
+ /** */
+ private final Ignite ignite;
+
+ /** Required by qryStr field query type for binary manipulation */
+ private IgniteBinaryImpl igniteBinary;
+
+ /** */
+ private BinaryType igniteBinType;
+
+ /** */
+ private final Method mtd;
+
+ /** */
+ private final RepositoryMetadata metadata;
+
+ /** */
+ private final ProjectionFactory factory;
+
+ /** */
+ private final ReturnStrategy staticReturnStgy;
+
+ /** Detect if returned data from method is projected */
+ private final boolean hasProjection;
+
+ /** Whether projection is dynamic (provided as method parameter) */
+ private final boolean hasDynamicProjection;
+
+ /** Dynamic projection parameter index */
+ private final int dynamicProjectionIndex;
+
+ /** Dynamic query configuration */
+ private final int dynamicQueryConfigurationIndex;
+
+ /** The return query method */
+ private final QueryMethod qMethod;
+
+ /** The return domain class of QueryMethod */
+ private final Class<?> returnedDomainClass;
+
+ /** */
+ private final SpelExpressionParser expressionParser;
+
+ /** Could provide ExtensionAwareQueryMethodEvaluationContextProvider */
+ private final QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider;
+
+ /** Static query configuration. */
+ private final DynamicQueryConfig staticQueryConfiguration;
+
+ /**
+ * Instantiates a new Ignite repository query.
+ *
+ * @param ignite the ignite
+ * @param metadata Metadata.
+ * @param staticQuery Query.
+ * @param mtd Method.
+ * @param factory Factory.
+ * @param cache Cache.
+ * @param staticQueryConfiguration the query configuration
+ * @param queryMethodEvaluationContextProvider the query method evaluation context provider
+ */
+ public IgniteRepositoryQuery(Ignite ignite,
+ RepositoryMetadata metadata,
+ @Nullable IgniteQuery staticQuery,
+ Method mtd,
+ ProjectionFactory factory,
+ IgniteCache cache,
+ @Nullable DynamicQueryConfig staticQueryConfiguration,
+ QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider) {
+ this.metadata = metadata;
+ this.mtd = mtd;
+ this.factory = factory;
+ type = metadata.getDomainType();
+
+ this.cache = cache;
+ this.ignite = ignite;
+
+ this.staticQueryConfiguration = staticQueryConfiguration;
+ this.staticQuery = staticQuery;
+
+ if (this.staticQuery != null)
+ staticReturnStgy = calcReturnType(mtd, this.staticQuery.isFieldQuery());
+ else
+ staticReturnStgy = null;
+
+ expressionParser = new SpelExpressionParser();
+ this.queryMethodEvaluationContextProvider = queryMethodEvaluationContextProvider;
+
+ qMethod = getQueryMethod();
+
+ // control projection
+ hasDynamicProjection = getQueryMethod().getParameters().hasDynamicProjection();
+ hasProjection = hasDynamicProjection || getQueryMethod().getResultProcessor().getReturnedType()
+ .isProjecting();
+
+ dynamicProjectionIndex = qMethod.getParameters().getDynamicProjectionIndex();
+
+ returnedDomainClass = getQueryMethod().getReturnedObjectType();
+
+ dynamicQueryConfigurationIndex = getDynamicQueryConfigurationIndex(qMethod);
+
+ // ensure dynamic query configuration param exists if dynamicQuery = true
+ if (dynamicQueryConfigurationIndex == -1 && this.staticQuery == null) {
+ throw new IllegalStateException(
+ "When passing dynamicQuery = true via org.apache.ignite.springdata.repository.config.Query "
+ + "annotation, you must provide a non null method parameter of type DynamicQueryConfig");
+ }
+ // ensure domain class is registered on marshaller to transform row to entity
+ registerClassOnMarshaller(((IgniteEx)ignite).context(), type);
+ }
+
+ /**
+ * {@inheritDoc} @param values the values
+ *
+ * @return the object
+ */
+ @Override public Object execute(Object[] values) {
+ Object[] parameters = values;
+
+ // config via Query annotation (dynamicQuery = false)
+ DynamicQueryConfig config = staticQueryConfiguration;
+
+ // or condition to allow query tunning
+ if (config == null || dynamicQueryConfigurationIndex != -1) {
+ DynamicQueryConfig newConfig = (DynamicQueryConfig)values[dynamicQueryConfigurationIndex];
+ parameters = ArrayUtils.removeElement(parameters, dynamicQueryConfigurationIndex);
+ if (newConfig != null) {
+ // upset query configuration
+ config = newConfig;
+ }
+ }
+ // query configuration is required, via Query annotation or per parameter (within provided values param)
+ if (config == null) {
+ throw new IllegalStateException(
+ "Unable to execute query. When passing dynamicQuery = true via org.apache.ignite.springdata"
+ + ".repository.config.Query annotation, you must provide a non null method parameter of type "
+ + "DynamicQueryConfig");
+ }
+
+ IgniteQuery qry = getQuery(config);
+
+ ReturnStrategy returnStgy = getReturnStgy(qry);
+
+ Query iQry = prepareQuery(qry, config, returnStgy, parameters);
+
+ QueryCursor qryCursor = cache.query(iQry);
+
+ return transformQueryCursor(qry, returnStgy, parameters, qryCursor);
+ }
+
+ /** {@inheritDoc} */
+ @Override public QueryMethod getQueryMethod() {
+ return new QueryMethod(mtd, metadata, factory);
+ }
+
+ private <T extends Parameter> int getDynamicQueryConfigurationIndex(QueryMethod method) {
+ Iterator<T> it = (Iterator<T>)method.getParameters().iterator();
+ int i = 0;
+ boolean found = false;
+ int index = -1;
+ while (it.hasNext()) {
+ T parameter = it.next();
+
+ if (DynamicQueryConfig.class.isAssignableFrom(parameter.getType())) {
+ if (found) {
+ throw new IllegalStateException("Invalid '" + method.getName() + "' repository method signature. "
+ + "Only ONE DynamicQueryConfig parameter is allowed");
+ }
+
+ found = true;
+ index = i;
+ }
+
+ i++;
+ }
+ return index;
+ }
+
+ /** */
+ private synchronized IgniteBinaryImpl binary() {
+ if (igniteBinary == null)
+ igniteBinary = (IgniteBinaryImpl)ignite.binary();
+
+ return igniteBinary;
+ }
+
+ /** */
+ private synchronized BinaryType binType() {
+ if (igniteBinType == null)
+ igniteBinType = binary().type(type);
+
+ return igniteBinType;
+ }
+
+ /**
+ * @param mtd Method.
+ * @param isFieldQry Is field query.
+ * @return Return strategy type.
+ */
+ private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) {
+ Class<?> returnType = mtd.getReturnType();
+
+ if (returnType == Slice.class) {
+ if (isFieldQry) {
+ if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+ return ReturnStrategy.SLICE_OF_LISTS;
+ }
+ else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+ return ReturnStrategy.SLICE_OF_CACHE_ENTRIES;
+ return ReturnStrategy.SLICE_OF_VALUES;
+ }
+ else if (returnType == Page.class)
+ return ReturnStrategy.PAGE_OF_VALUES;
+ else if (returnType == Stream.class)
+ return ReturnStrategy.STREAM_OF_VALUES;
+ else if (Cache.Entry.class.isAssignableFrom(returnType))
+ return ReturnStrategy.CACHE_ENTRY;
+ else if (Iterable.class.isAssignableFrom(returnType)) {
+ if (isFieldQry) {
+ if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+ return ReturnStrategy.LIST_OF_LISTS;
+ }
+ else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+ return ReturnStrategy.LIST_OF_CACHE_ENTRIES;
+ return ReturnStrategy.LIST_OF_VALUES;
+ }
+ else
+ return ReturnStrategy.ONE_VALUE;
+ }
+
+ /**
+ * @param cls Class.
+ * @param mtd Method.
+ * @return if {@code mtd} return type is assignable from {@code cls}
+ */
+ private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
+ Type genericReturnType = mtd.getGenericReturnType();
+
+ if (!(genericReturnType instanceof ParameterizedType))
+ return false;
+
+ Type[] actualTypeArguments = ((ParameterizedType)genericReturnType).getActualTypeArguments();
+
+ if (actualTypeArguments.length == 0)
+ return false;
+
+ if (actualTypeArguments[0] instanceof ParameterizedType) {
+ ParameterizedType type = (ParameterizedType)actualTypeArguments[0];
+
+ Class<?> type1 = (Class)type.getRawType();
+
+ return type1.isAssignableFrom(cls);
+ }
+
+ if (actualTypeArguments[0] instanceof Class) {
+ Class typeArg = (Class)actualTypeArguments[0];
+
+ return typeArg.isAssignableFrom(cls);
+ }
+
+ return false;
+ }
+
+ /**
+ * When select fields by query H2 returns Timestamp for types java.util.Date and java.qryStr.Timestamp
+ *
+ * @see org.apache.ignite.internal.processors.query.h2.H2DatabaseType map.put(Timestamp.class, TIMESTAMP)
+ * map.put(java.util.Date.class, TIMESTAMP) map.put(java.qryStr.Date.class, DATE)
+ */
+ private static <T> T fixExpectedType(final Object object, final Class<T> expected) {
+ if (expected != null && object instanceof java.sql.Timestamp && expected.equals(java.util.Date.class))
+ return (T)new java.util.Date(((java.sql.Timestamp)object).getTime());
+
+ return (T)object;
+ }
+
+ /**
+ * @param cfg Config.
+ */
+ private IgniteQuery getQuery(@Nullable DynamicQueryConfig cfg) {
+ if (staticQuery != null)
+ return staticQuery;
+
+ if (cfg != null && (StringUtils.hasText(cfg.value()) || cfg.textQuery())) {
+ return new IgniteQuery(cfg.value(),
+ !cfg.textQuery() && (isFieldQuery(cfg.value()) || cfg.forceFieldsQuery()), cfg.textQuery(),
+ false, IgniteQueryGenerator.getOptions(mtd));
+ }
+
+ throw new IllegalStateException("Unable to obtain a valid query. When passing dynamicQuery = true via org"
+ + ".apache.ignite.springdata.repository.config.Query annotation, you must"
+ + " provide a non null method parameter of type DynamicQueryConfig with a "
+ + "non empty value (query string) or textQuery = true");
+ }
+
+ /**
+ * @param qry Query.
+ */
+ private ReturnStrategy getReturnStgy(IgniteQuery qry) {
+ if (staticReturnStgy != null)
+ return staticReturnStgy;
+
+ if (qry != null)
+ return calcReturnType(mtd, qry.isFieldQuery());
+
+ throw new IllegalStateException("Unable to obtain a valid return strategy. When passing dynamicQuery = true "
+ + "via org.apache.ignite.springdata.repository.config.Query annotation, "
+ + "you must provide a non null method parameter of type "
+ + "DynamicQueryConfig with a non empty value (query string) or textQuery "
+ + "= true");
+ }
+
+ /**
+ * @param cls Class.
+ */
+ private static boolean isPrimitiveOrWrapper(Class<?> cls) {
+ return cls.isPrimitive() ||
+ Boolean.class.equals(cls) ||
+ Byte.class.equals(cls) ||
+ Character.class.equals(cls) ||
+ Short.class.equals(cls) ||
+ Integer.class.equals(cls) ||
+ Long.class.equals(cls) ||
+ Float.class.equals(cls) ||
+ Double.class.equals(cls) ||
+ Void.class.equals(cls) ||
+ String.class.equals(cls) ||
+ UUID.class.equals(cls);
+ }
+
+ /**
+ * @param prmtrs Prmtrs.
+ * @param qryCursor Query cursor.
+ * @return Query cursor or slice
+ */
+ @Nullable
+ private Object transformQueryCursor(IgniteQuery qry,
+ ReturnStrategy returnStgy,
+ Object[] prmtrs,
+ QueryCursor qryCursor) {
+ final Class<?> returnClass;
+
+ if (hasProjection) {
+ if (hasDynamicProjection)
+ returnClass = (Class<?>)prmtrs[dynamicProjectionIndex];
+ else
+ returnClass = returnedDomainClass;
+ }
+ else
+ returnClass = returnedDomainClass;
+
+ if (qry.isFieldQuery()) {
+ // take control over single primite result from queries, i.e. DELETE, SELECT COUNT, UPDATE ...
+ boolean singlePrimitiveResult = isPrimitiveOrWrapper(returnClass);
+
+ final List<GridQueryFieldMetadata> meta = ((QueryCursorEx)qryCursor).fieldsMeta();
+
+ Function<List<?>, ?> cWrapperTransformFunction = null;
+
+ if (type.equals(returnClass)) {
+ IgniteBinaryImpl binary = binary();
+ BinaryType binType = binType();
+ cWrapperTransformFunction = row -> rowToEntity(binary, binType, row, meta);
+ }
+ else {
+ if (hasProjection || singlePrimitiveResult) {
+ if (singlePrimitiveResult)
+ cWrapperTransformFunction = row -> row.get(0);
+ else {
+ // Map row -> projection class
+ cWrapperTransformFunction = row -> factory
+ .createProjection(returnClass, rowToMap(row, meta));
+ }
+ }
+ else
+ cWrapperTransformFunction = row -> rowToMap(row, meta);
+ }
+
+ QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<List<?>>)qryCursor,
+ cWrapperTransformFunction);
+
+ switch (returnStgy) {
+ case PAGE_OF_VALUES:
+ return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+ case LIST_OF_VALUES:
+ return cWrapper.getAll();
+ case STREAM_OF_VALUES:
+ return cWrapper.stream();
+ case ONE_VALUE:
+ Iterator<?> iter = cWrapper.iterator();
+ if (iter.hasNext()) {
+ Object resp = iter.next();
+ U.closeQuiet(cWrapper);
+ return resp;
+ }
+ return null;
+ case SLICE_OF_VALUES:
+ return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case SLICE_OF_LISTS:
+ return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case LIST_OF_LISTS:
+ return qryCursor.getAll();
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ else {
+ Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor;
+
+ Function<CacheEntryImpl, ?> cWrapperTransformFunction;
+
+ if (hasProjection && !type.equals(returnClass))
+ cWrapperTransformFunction = row -> factory.createProjection(returnClass, row.getValue());
+ else
+ cWrapperTransformFunction = row -> row.getValue();
+
+ QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<CacheEntryImpl>)qryCursor,
+ cWrapperTransformFunction);
+
+ switch (returnStgy) {
+ case PAGE_OF_VALUES:
+ return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+ case LIST_OF_VALUES:
+ return cWrapper.getAll();
+ case STREAM_OF_VALUES:
+ return cWrapper.stream();
+ case ONE_VALUE:
+ Iterator<?> iter1 = cWrapper.iterator();
+ if (iter1.hasNext()) {
+ Object resp = iter1.next();
+ U.closeQuiet(cWrapper);
+ return resp;
+ }
+ return null;
+ case CACHE_ENTRY:
+ Iterator<?> iter2 = qryIter.iterator();
+ if (iter2.hasNext()) {
+ Object resp2 = iter2.next();
+ U.closeQuiet(qryCursor);
+ return resp2;
+ }
+ return null;
+ case SLICE_OF_VALUES:
+ return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case SLICE_OF_CACHE_ENTRIES:
+ return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+ case LIST_OF_CACHE_ENTRIES:
+ return qryCursor.getAll();
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * Extract bindable values
+ *
+ * @param values values invoking query method
+ * @param queryMethodParams query method parameter definitions
+ * @param queryBindings All parameters found on query string that need to be binded
+ * @return new list of parameters
+ */
+ private Object[] extractBindableValues(Object[] values,
+ Parameters<?, ?> queryMethodParams,
+ List<ParameterBinding> queryBindings) {
+ // no binding params then exit
+ if (queryBindings.isEmpty())
+ return values;
+
+ Object[] newValues = new Object[queryBindings.size()];
+
+ // map bindable parameters from query method: (index/name) - index
+ HashMap<String, Integer> methodParams = new HashMap<>();
+
+ // create an evaluation context for custom query
+ EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+ .getEvaluationContext(queryMethodParams, values);
+
+ // By default queryEvalContext:
+ // - make accesible query method parameters by index:
+ // @Query("select u from User u where u.age = ?#{[0]}")
+ // List<User> findUsersByAge(int age);
+ // - make accesible query method parameters by name:
+ // @Query("select u from User u where u.firstname = ?#{#customer.firstname}")
+ // List<User> findUsersByCustomersFirstname(@Param("customer") Customer customer);
+
+ // query method param's index by name and position
+ queryMethodParams.getBindableParameters().forEach(p -> {
+ if (p.isNamedParameter()) {
+ // map by name (annotated by @Param)
+ methodParams.put(p.getName().get(), p.getIndex());
+ }
+ // map by position
+ methodParams.put(String.valueOf(p.getIndex()), p.getIndex());
+ });
+
+ // process all parameters on query and extract new values to bind
+ for (int i = 0; i < queryBindings.size(); i++) {
+ ParameterBinding p = queryBindings.get(i);
+
+ if (p.isExpression()) {
+ // Evaluate SpEl expressions (synthetic parameter value) , example ?#{#customer.firstname}
+ newValues[i] = expressionParser.parseExpression(p.getExpression()).getValue(queryEvalContext);
+ }
+ else {
+ // Extract parameter value by name or position respectively from invoking values
+ newValues[i] = values[methodParams.get(
+ p.getName() != null ? p.getName() : String.valueOf(p.getRequiredPosition() - 1))];
+ }
+ }
+
+ return newValues;
+ }
+
+ /**
+ * @param qry Query.
+ * @param config Config.
+ * @param returnStgy Return stgy.
+ * @param values Values.
+ * @return prepared query for execution
+ */
+ private Query prepareQuery(IgniteQuery qry, DynamicQueryConfig config, ReturnStrategy returnStgy, Object[] values) {
+ Object[] parameters = values;
+
+ String queryString = qry.qryStr();
+
+ Query query;
+
+ checkRequiredPageable(returnStgy, values);
+
+ if (!qry.isTextQuery()) {
+ if (!qry.isAutogenerated()) {
+ StringQuery squery = new ExpressionBasedStringQuery(queryString, metadata, expressionParser);
+ queryString = squery.getQueryString();
+ parameters = extractBindableValues(parameters, getQueryMethod().getParameters(),
+ squery.getParameterBindings());
+ }
+ else {
+ // remove dynamic projection from parameters
+ if (hasDynamicProjection)
+ parameters = ArrayUtils.remove(parameters, dynamicProjectionIndex);
+ }
+
+ switch (qry.options()) {
+ case SORTING:
+ queryString = IgniteQueryGenerator
+ .addSorting(new StringBuilder(queryString), (Sort)values[values.length - 1])
+ .toString();
+ if (qry.isAutogenerated())
+ parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+ break;
+ case PAGINATION:
+ queryString = IgniteQueryGenerator
+ .addPaging(new StringBuilder(queryString), (Pageable)values[values.length - 1])
+ .toString();
+ if (qry.isAutogenerated())
+ parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+ break;
+ default:
+ }
+
+ if (qry.isFieldQuery()) {
+ SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(queryString);
+ sqlFieldsQry.setArgs(parameters);
+
+ sqlFieldsQry.setCollocated(config.collocated());
+ sqlFieldsQry.setDistributedJoins(config.distributedJoins());
+ sqlFieldsQry.setEnforceJoinOrder(config.enforceJoinOrder());
+ sqlFieldsQry.setLazy(config.lazy());
+ sqlFieldsQry.setLocal(config.local());
+
+ if (config.parts() != null && config.parts().length > 0)
+ sqlFieldsQry.setPartitions(config.parts());
+
+ sqlFieldsQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+ query = sqlFieldsQry;
+ }
+ else {
+ SqlQuery sqlQry = new SqlQuery(type, queryString);
+ sqlQry.setArgs(parameters);
+
+ sqlQry.setDistributedJoins(config.distributedJoins());
+ sqlQry.setLocal(config.local());
+
+ if (config.parts() != null && config.parts().length > 0)
+ sqlQry.setPartitions(config.parts());
+
+ sqlQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+ query = sqlQry;
+ }
+ }
+ else {
+ int pageSize = -1;
+
+ switch (qry.options()) {
+ case PAGINATION:
+ pageSize = ((Pageable)parameters[parameters.length - 1]).getPageSize();
+ break;
+ }
+
+ // check if queryString contains SpEL template expressions and evaluate them if any
+ if (queryString.contains("#{")) {
+ EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+ .getEvaluationContext(getQueryMethod().getParameters(),
+ values);
+
+ Object eval = expressionParser.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION)
+ .getValue(queryEvalContext);
+
+ if (!(eval instanceof String)) {
+ throw new IllegalStateException(
+ "TextQuery with SpEL expressions must produce a String response, but found " + eval.getClass()
+ .getName()
+ + ". Please, check your expression: " + queryString);
+ }
+ queryString = (String)eval;
+ }
+
+ TextQuery textQuery = new TextQuery(type, queryString, config.limit());
+
+ textQuery.setLocal(config.local());
+
+ if (pageSize > -1)
+ textQuery.setPageSize(pageSize);
+
+ query = textQuery;
+ }
+ return query;
+ }
+
+ /** */
+ private static Map<String, Object> rowToMap(final List<?> row, final List<GridQueryFieldMetadata> meta) {
+ // use treemap with case insensitive property name
+ final TreeMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0; i < meta.size(); i++) {
+ // don't want key or val columns
+ final String metaField = meta.get(i).fieldName().toLowerCase();
+ if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+ QueryUtils.VAL_FIELD_NAME))
+ map.put(metaField, row.get(i));
+ }
+ return map;
+ }
+
+ /**
+ * convert row ( with list of field values) into domain entity
+ */
+ private <V> V rowToEntity(final IgniteBinaryImpl binary,
+ final BinaryType binType,
+ final List<?> row,
+ final List<GridQueryFieldMetadata> meta) {
+ // additional data returned by query not present on domain object type
+ final TreeMap<String, Object> metadata = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ final BinaryObjectBuilder bldr = binary.builder(binType.typeName());
+
+ for (int i = 0; i < row.size(); i++) {
+ final GridQueryFieldMetadata fMeta = meta.get(i);
+ final String metaField = fMeta.fieldName();
+ // add existing entity fields to binary object
+ if (binType.field(fMeta.fieldName()) != null && !metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME)
+ && !metaField.equalsIgnoreCase(QueryUtils.VAL_FIELD_NAME)) {
+ final Object fieldValue = row.get(i);
+ if (fieldValue != null) {
+ final Class<?> clazz = getClassForBinaryField(binary, binType, fMeta);
+ // null values must not be set into binary objects
+ bldr.setField(metaField, fixExpectedType(fieldValue, clazz));
+ }
+ }
+ else {
+ // don't want key or val column... but wants null values
+ if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+ QueryUtils.VAL_FIELD_NAME))
+ metadata.put(metaField, row.get(i));
+ }
+ }
+ return bldr.build().deserialize();
+ }
+
+ /**
+ * Obtains real field class from resultset metadata field whether it's available
+ */
+ private Class<?> getClassForBinaryField(final IgniteBinaryImpl binary,
+ final BinaryType binType,
+ final GridQueryFieldMetadata fieldMeta) {
+ try {
+ final String fieldId = fieldMeta.schemaName() + "." + fieldMeta.typeName() + "." + fieldMeta.fieldName();
+
+ if (binaryFieldClass.containsKey(fieldId))
+ return binaryFieldClass.get(fieldId);
+
+ Class<?> clazz = null;
+
+ synchronized (binaryFieldClass) {
+
+ if (binaryFieldClass.containsKey(fieldId))
+ return binaryFieldClass.get(fieldId);
+
+ String fieldName = null;
+
+ // search field name on binary type (query returns case insensitive
+ // field name) but BinaryType is not case insensitive
+ for (final String fname : binType.fieldNames()) {
+ if (fname.equalsIgnoreCase(fieldMeta.fieldName())) {
+ fieldName = fname;
+ break;
+ }
+ }
+
+ final CacheObjectBinaryProcessorImpl proc = (CacheObjectBinaryProcessorImpl)binary.processor();
+
+ // search for class by typeId, if not found use
+ // fieldMeta.fieldTypeName() class
+ clazz = BinaryUtils.resolveClass(proc.binaryContext(), binary.typeId(binType.fieldTypeName(fieldName)),
+ fieldMeta.fieldTypeName(), ignite.configuration().getClassLoader(), true);
+
+ binaryFieldClass.put(fieldId, clazz);
+ }
+
+ return clazz;
+ }
+ catch (final Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Validates operations that requires Pageable parameter
+ *
+ * @param returnStgy Return stgy.
+ * @param prmtrs Prmtrs.
+ */
+ private void checkRequiredPageable(ReturnStrategy returnStgy, Object[] prmtrs) {
+ try {
+ if (returnStgy == ReturnStrategy.PAGE_OF_VALUES || returnStgy == ReturnStrategy.SLICE_OF_VALUES
+ || returnStgy == ReturnStrategy.SLICE_OF_CACHE_ENTRIES) {
+ Pageable page = (Pageable)prmtrs[prmtrs.length - 1];
+ page.isPaged();
+ }
+ }
+ catch (NullPointerException | IndexOutOfBoundsException | ClassCastException e) {
+ throw new IllegalStateException(
+ "For " + returnStgy.name() + " you must provide on last method parameter a non null Pageable instance");
+ }
+ }
+
+ /**
+ * @param ctx Context.
+ * @param clazz Clazz.
+ */
+ private static void registerClassOnMarshaller(final GridKernalContext ctx, final Class<?> clazz) {
+ try {
+ // ensure class registration for marshaller on cluster...
+ if (!U.isJdk(clazz))
+ U.marshal(ctx, clazz.newInstance());
+ }
+ catch (final Exception ignored) {
+ // silent
+ }
+ }
+
+ /**
+ * Ignite QueryCursor wrapper.
+ * <p>
+ * Ensures closing underline cursor when there is no data.
+ *
+ * @param <T> input type
+ * @param <V> transformed output type
+ */
+ public static class QueryCursorWrapper<T, V> extends AbstractCollection<V> implements QueryCursor<V> {
+ /**
+ * Delegate query cursor.
+ */
+ private final QueryCursor<T> delegate;
+
+ /**
+ * Transformer.
+ */
+ private final Function<T, V> transformer;
+
+ /**
+ * Instantiates a new Query cursor wrapper.
+ *
+ * @param delegate delegate QueryCursor with T input elements
+ * @param transformer Function to transform T to V elements
+ */
+ public QueryCursorWrapper(final QueryCursor<T> delegate, final Function<T, V> transformer) {
+ this.delegate = delegate;
+ this.transformer = transformer;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterator<V> iterator() {
+ final Iterator<T> it = delegate.iterator();
+
+ return new Iterator<V>() {
+ /** */
+ @Override public boolean hasNext() {
+ if (!it.hasNext()) {
+ U.closeQuiet(delegate);
+ return false;
+ }
+ return true;
+ }
+
+ /** */
+ @Override public V next() {
+ final V r = transformer.apply(it.next());
+ if (r != null)
+ return r;
+ throw new NoSuchElementException();
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override public void close() {
+ U.closeQuiet(delegate);
+ }
+
+ /** {@inheritDoc} */
+ @Override public List<V> getAll() {
+ final List<V> data = new ArrayList<>();
+ delegate.forEach(i -> data.add(transformer.apply(i)));
+ U.closeQuiet(delegate);
+ return data;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int size() {
+ return 0;
+ }
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/QueryUtils.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/QueryUtils.java
new file mode 100644
index 0000000..711d297
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/QueryUtils.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2008-2019 the original author or authors.
+ *
+ * Licensed 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.springdata22.repository.query;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.util.Streamable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.compile;
+
+/**
+ * Simple utility class to create queries.
+ *
+ * @author Oliver Gierke
+ * @author Kevin Raymond
+ * @author Thomas Darimont
+ * @author Komi Innocent
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Sébastien Péralta
+ * @author Jens Schauder
+ * @author Nils Borrmann
+ * @author Reda.Housni -Alaoui
+ */
+public abstract class QueryUtils {
+ /**
+ * The constant COUNT_QUERY_STRING.
+ */
+ public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
+
+ /**
+ * The constant DELETE_ALL_QUERY_STRING.
+ */
+ public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
+
+ /**
+ * Used Regex/Unicode categories (see http://www.unicode.org/reports/tr18/#General_Category_Property): Z Separator
+ * Cc Control Cf Format P Punctuation
+ */
+ private static final String IDENTIFIER = "[._[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{P}]]+";
+
+ /**
+ * The Colon no double colon.
+ */
+ static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
+
+ /**
+ * The Identifier group.
+ */
+ static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
+
+ /** */
+ private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
+
+ /** */
+ private static final String SIMPLE_COUNT_VALUE = "$2";
+
+ /** */
+ private static final String COMPLEX_COUNT_VALUE = "$3$6";
+
+ /** */
+ private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*$";
+
+ /** */
+ private static final Pattern ALIAS_MATCH;
+
+ /** */
+ private static final Pattern COUNT_MATCH;
+
+ /** */
+ private static final Pattern PROJECTION_CLAUSE = Pattern
+ .compile("select\\s+(.+)\\s+from", Pattern.CASE_INSENSITIVE);
+
+ /** */
+ private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
+
+ /** */
+ private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
+
+ /** */
+ private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
+
+ /** */
+ private static final Pattern NAMED_PARAMETER = Pattern.compile(
+ COLON_NO_DOUBLE_COLON + IDENTIFIER + "|\\#" + IDENTIFIER, CASE_INSENSITIVE);
+
+ /** */
+ private static final Pattern CONSTRUCTOR_EXPRESSION;
+
+ /** */
+ private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
+
+ /** */
+ private static final int VARIABLE_NAME_GROUP_INDEX = 4;
+
+ /** */
+ private static final Pattern FUNCTION_PATTERN;
+
+ static {
+ StringBuilder builder = new StringBuilder();
+ builder.append("(?<=from)"); // from as starting delimiter
+ builder.append("(?:\\s)+"); // at least one space separating
+ builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
+ builder.append("(?:\\sas)*"); // exclude possible "as" keyword
+ builder.append("(?:\\s)+"); // at least one space separating
+ builder.append("(?!(?:where))(\\w+)"); // the actual alias
+
+ ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+ builder = new StringBuilder();
+ builder.append("(select\\s+((distinct )?(.+?)?)\\s+)?(from\\s+");
+ builder.append(IDENTIFIER);
+ builder.append("(?:\\s+as)?\\s+)");
+ builder.append(IDENTIFIER_GROUP);
+ builder.append("(.*)");
+
+ COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+ builder = new StringBuilder();
+ builder.append("select");
+ builder.append("\\s+"); // at least one space separating
+ builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
+ builder.append("new");
+ builder.append("\\s+"); // at least one space separating
+ builder.append(IDENTIFIER);
+ builder.append("\\s*"); // zero to unlimited space separating
+ builder.append("\\(");
+ builder.append(".*");
+ builder.append("\\)");
+
+ CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);
+
+ builder = new StringBuilder();
+ // any function call including parameters within the brackets
+ builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
+ // the potential alias
+ builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
+
+ FUNCTION_PATTERN = compile(builder.toString());
+ }
+
+ /**
+ * Private constructor to prevent instantiation.
+ */
+ private QueryUtils() {
+ // No-op.
+ }
+
+ /**
+ * Returns the query string to execute an exists query for the given id attributes.
+ *
+ * @param entityName the name of the entity to create the query for, must not be {@literal null}.
+ * @param cntQryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
+ * @param idAttrs the id attributes for the entity, must not be {@literal null}.
+ * @return the exists query string
+ */
+ public static String getExistsQueryString(String entityName,
+ String cntQryPlaceHolder,
+ Iterable<String> idAttrs) {
+ String whereClause = Streamable.of(idAttrs).stream() //
+ .map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute,
+ idAttribute)) //
+ .collect(Collectors.joining(" AND ", " WHERE ", ""));
+
+ return String.format(COUNT_QUERY_STRING, cntQryPlaceHolder, entityName) + whereClause;
+ }
+
+ /**
+ * Returns the query string for the given class name.
+ *
+ * @param template must not be {@literal null}.
+ * @param entityName must not be {@literal null}.
+ * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal
+ * null}.
+ */
+ public static String getQueryString(String template, String entityName) {
+ Assert.hasText(entityName, "Entity name must not be null or empty!");
+
+ return String.format(template, entityName);
+ }
+
+ /**
+ * Returns the aliases used for {@code left (outer) join}s.
+ *
+ * @param qry a query string to extract the aliases of joins from. Must not be {@literal null}.
+ * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
+ */
+ static Set<String> getOuterJoinAliases(String qry) {
+ Set<String> result = new HashSet<>();
+ Matcher matcher = JOIN_PATTERN.matcher(qry);
+
+ while (matcher.find()) {
+ String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
+ if (StringUtils.hasText(alias))
+ result.add(alias);
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
+ *
+ * @param qry a {@literal String} containing a query. Must not be {@literal null}.
+ * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
+ */
+ static Set<String> getFunctionAliases(String qry) {
+ Set<String> result = new HashSet<>();
+ Matcher matcher = FUNCTION_PATTERN.matcher(qry);
+
+ while (matcher.find()) {
+ String alias = matcher.group(1);
+
+ if (StringUtils.hasText(alias))
+ result.add(alias);
+ }
+
+ return result;
+ }
+
+ /**
+ * Resolves the alias for the entity to be retrieved from the given JPA query.
+ *
+ * @param qry must not be {@literal null}.
+ * @return Might return {@literal null}.
+ */
+ @Nullable
+ static String detectAlias(String qry) {
+ Matcher matcher = ALIAS_MATCH.matcher(qry);
+
+ return matcher.find() ? matcher.group(2) : null;
+ }
+
+ /**
+ * Creates a count projected query from the given original query.
+ *
+ * @param originalQry must not be {@literal null}.
+ * @param cntProjection may be {@literal null}.
+ * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
+ */
+ static String createCountQueryFor(String originalQry, @Nullable String cntProjection) {
+ Assert.hasText(originalQry, "OriginalQuery must not be null or empty!");
+
+ Matcher matcher = COUNT_MATCH.matcher(originalQry);
+ String countQuery;
+
+ if (cntProjection == null) {
+ String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
+ boolean useVariable = variable != null && StringUtils.hasText(variable) && !variable.startsWith("new")
+ && !variable.startsWith("count(") && !variable.contains(",");
+
+ String replacement = useVariable ? SIMPLE_COUNT_VALUE : COMPLEX_COUNT_VALUE;
+ countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
+ }
+ else
+ countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, cntProjection));
+
+ return countQuery.replaceFirst(ORDER_BY_PART, "");
+ }
+
+ /**
+ * Returns whether the given JPQL query contains a constructor expression.
+ *
+ * @param qry must not be {@literal null} or empty.
+ * @return boolean
+ */
+ public static boolean hasConstructorExpression(String qry) {
+ Assert.hasText(qry, "Query must not be null or empty!");
+
+ return CONSTRUCTOR_EXPRESSION.matcher(qry).find();
+ }
+
+ /**
+ * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
+ *
+ * @param qry must not be {@literal null} or empty.
+ * @return projection
+ */
+ public static String getProjection(String qry) {
+ Assert.hasText(qry, "Query must not be null or empty!");
+
+ Matcher matcher = PROJECTION_CLAUSE.matcher(qry);
+ String projection = matcher.find() ? matcher.group(1) : "";
+ return projection.trim();
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/StringQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/StringQuery.java
new file mode 100644
index 0000000..8957e69
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/StringQuery.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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.springdata22.repository.query;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.repository.query.SpelQueryContext;
+import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
+import org.springframework.data.repository.query.parser.Part.Type;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static org.springframework.util.ObjectUtils.nullSafeEquals;
+import static org.springframework.util.ObjectUtils.nullSafeHashCode;
+
+/**
+ * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned
+ * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations
+ * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
+ * with synthetic bind parameters
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Oliver Wehrens
+ * @author Mark Paluch
+ * @author Jens Schauder
+ */
+class StringQuery implements DeclaredQuery {
+ /** */
+ private final String query;
+
+ /** */
+ private final List<ParameterBinding> bindings;
+
+ /** */
+ @Nullable
+ private final String alias;
+
+ /** */
+ private final boolean hasConstructorExpression;
+
+ /** */
+ private final boolean containsPageableInSpel;
+
+ /** */
+ private final boolean usesJdbcStyleParameters;
+
+ /**
+ * Creates a new {@link StringQuery} from the given JPQL query.
+ *
+ * @param query must not be {@literal null} or empty.
+ */
+ StringQuery(String query) {
+ Assert.hasText(query, "Query must not be null or empty!");
+
+ bindings = new ArrayList<>();
+ containsPageableInSpel = query.contains("#pageable");
+
+ Metadata queryMeta = new Metadata();
+ this.query = ParameterBindingParser.INSTANCE
+ .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, bindings,
+ queryMeta);
+
+ usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
+ alias = QueryUtils.detectAlias(query);
+ hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
+ }
+
+ /**
+ * Returns whether we have found some like bindings.
+ */
+ boolean hasParameterBindings() {
+ return !bindings.isEmpty();
+ }
+
+ /** */
+ String getProjection() {
+ return QueryUtils.getProjection(query);
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#getParameterBindings()
+ /** {@inheritDoc} */
+ @Override public List<ParameterBinding> getParameterBindings() {
+ return bindings;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#deriveCountQuery(java.lang.String, java.lang
+ /** {@inheritDoc} */
+ @Override public DeclaredQuery deriveCountQuery(@Nullable String countQuery,
+ @Nullable String countQueryProjection) {
+ return DeclaredQuery
+ .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesJdbcStyleParameters()
+ /** */
+ @Override public boolean usesJdbcStyleParameters() {
+ return usesJdbcStyleParameters;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#getQueryString()
+ /** {@inheritDoc} */
+ @Override public String getQueryString() {
+ return query;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#getAlias()
+ /** {@inheritDoc} */
+ @Override @Nullable
+ public String getAlias() {
+ return alias;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasConstructorExpression()
+ /** {@inheritDoc} */
+ @Override public boolean hasConstructorExpression() {
+ return hasConstructorExpression;
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#isDefaultProjection()
+ /** {@inheritDoc} */
+ @Override public boolean isDefaultProjection() {
+ return getProjection().equalsIgnoreCase(alias);
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasNamedParameter()
+ /** {@inheritDoc} */
+ @Override public boolean hasNamedParameter() {
+ return bindings.stream().anyMatch(b -> b.getName() != null);
+ }
+
+ // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesPaging()
+ /** {@inheritDoc} */
+ @Override public boolean usesPaging() {
+ return containsPageableInSpel;
+ }
+
+ /**
+ * A parser that extracts the parameter bindings from a given query string.
+ *
+ * @author Thomas Darimont
+ */
+ enum ParameterBindingParser {
+ /** */
+ INSTANCE;
+
+ /** */
+ private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
+
+ /** */
+ public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
+ // .....................................................................^ not followed by a hash or a letter.
+ // .................................................................^ zero or more digits.
+ // .............................................................^ start with a question mark.
+
+ /** */
+ private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
+
+ /** */
+ private static final Pattern PARAMETER_BINDING_PATTERN;
+
+ /** */
+ private static final String MESSAGE =
+ "Already found parameter binding with same index / parameter name but differing binding type! "
+ + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same "
+ + "binding.";
+
+ /** */
+ private static final int INDEXED_PARAMETER_GROUP = 4;
+
+ /** */
+ private static final int NAMED_PARAMETER_GROUP = 6;
+
+ /** */
+ private static final int COMPARISION_TYPE_GROUP = 1;
+
+ static {
+ List<String> keywords = new ArrayList<>();
+
+ for (ParameterBindingType type : ParameterBindingType.values()) {
+ if (type.getKeyword() != null) {
+ keywords.add(type.getKeyword());
+ }
+ }
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("(");
+ builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
+ builder.append(")?");
+ builder.append("(?: )?"); // some whitespace
+ builder.append("\\(?"); // optional braces around parameters
+ builder.append("(");
+ builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
+ builder.append("|"); // or
+
+ // named parameter and the parameter name
+ builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
+
+ builder.append(")");
+ builder.append("\\)?"); // optional braces around parameters
+
+ PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
+ }
+
+ /**
+ * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings.
+ * Returns the cleaned up query.
+ */
+ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
+ List<ParameterBinding> bindings,
+ Metadata queryMeta) {
+ int greatestParamIdx = tryFindGreatestParameterIndexIn(query);
+ boolean parametersShouldBeAccessedByIdx = greatestParamIdx != -1;
+
+ /*
+ * Prefer indexed access over named parameters if only SpEL Expression parameters are present.
+ */
+ if (!parametersShouldBeAccessedByIdx && query.contains("?#{")) {
+ parametersShouldBeAccessedByIdx = true;
+ greatestParamIdx = 0;
+ }
+
+ SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIdx,
+ greatestParamIdx);
+
+ String resultingQry = spelExtractor.getQueryString();
+ Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQry);
+ QuotationMap quotedAreas = new QuotationMap(resultingQry);
+
+ int expressionParamIdx = parametersShouldBeAccessedByIdx ? greatestParamIdx : 0;
+
+ boolean usesJpaStyleParameters = false;
+
+ while (matcher.find()) {
+ if (quotedAreas.isQuoted(matcher.start()))
+ continue;
+
+ String paramIdxStr = matcher.group(INDEXED_PARAMETER_GROUP);
+ String paramName = paramIdxStr != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
+ Integer paramIdx = getParameterIndex(paramIdxStr);
+
+ String typeSrc = matcher.group(COMPARISION_TYPE_GROUP);
+ String expression = spelExtractor
+ .getParameter(paramName == null ? paramIdxStr : paramName);
+ String replacement = null;
+
+ Assert.isTrue(paramIdxStr != null || paramName != null,
+ () -> String.format("We need either a name or an index! Offending query string: %s", query));
+
+ expressionParamIdx++;
+ if (paramIdxStr != null && paramIdxStr.isEmpty()) {
+ queryMeta.usesJdbcStyleParameters = true;
+ paramIdx = expressionParamIdx;
+ }
+ else
+ usesJpaStyleParameters = true;
+
+ // named parameters (:param) will be untouched by spelExtractor, so replace them by ? as we don't
+ // know position
+ if (paramName != null)
+ replacement = "?";
+
+ if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
+ throw new IllegalArgumentException(
+ "Mixing of ? (? or :myNamedParam) parameters and other forms like ?1 (SpEL espressions or "
+ + "indexed) is not supported!. Please, if you are using expressions or "
+ + "indexed params, replace all named parameters by expressions. Example :myNamedParam "
+ + "by ?#{#myNamedParam}.");
+ }
+
+ switch (ParameterBindingType.of(typeSrc)) {
+ case LIKE:
+ Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
+ replacement = matcher.group(3);
+
+ if (paramIdx != null)
+ checkAndRegister(new LikeParameterBinding(paramIdx, likeType, expression), bindings);
+ else {
+ checkAndRegister(new LikeParameterBinding(paramName, likeType, expression), bindings);
+
+ replacement = expression != null ? ":" + paramName : matcher.group(5);
+ }
+
+ break;
+
+ case IN:
+ if (paramIdx != null)
+ checkAndRegister(new InParameterBinding(paramIdx, expression), bindings);
+ else
+ checkAndRegister(new InParameterBinding(paramName, expression), bindings);
+
+ break;
+
+ case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
+ default:
+ bindings.add(paramIdx != null
+ ? new ParameterBinding(null, paramIdx, expression)
+ : new ParameterBinding(paramName, null, expression));
+ }
+
+ if (replacement != null)
+ resultingQry = replaceFirst(resultingQry, matcher.group(2), replacement);
+ }
+
+ return resultingQry;
+ }
+
+ /** */
+ private static SpelExtractor createSpelExtractor(String queryWithSpel,
+ boolean parametersShouldBeAccessedByIndex,
+ int greatestParameterIndex) {
+
+ /*
+ * If parameters need to be bound by index, we bind the synthetic expression parameters starting from
+ * position of the greatest discovered index parameter in order to
+ * not mix-up with the actual parameter indices.
+ */
+ int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
+
+ BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
+ ? (index, expression) -> String.valueOf(
+ index + expressionParameterIndex + 1)
+ : (index, expression) ->
+ EXPRESSION_PARAMETER_PREFIX + (index
+ + 1);
+
+ String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
+
+ BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
+
+ return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
+ }
+
+ /** */
+ private static String replaceFirst(String text, String substring, String replacement) {
+ int index = text.indexOf(substring);
+ if (index < 0)
+ return text;
+
+ return text.substring(0, index) + replacement + text.substring(index + substring.length());
+ }
+
+ /** */
+ @Nullable
+ private static Integer getParameterIndex(@Nullable String parameterIndexString) {
+ if (parameterIndexString == null || parameterIndexString.isEmpty())
+ return null;
+ return Integer.valueOf(parameterIndexString);
+ }
+
+ /** */
+ private static int tryFindGreatestParameterIndexIn(String query) {
+ Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
+
+ int greatestParameterIndex = -1;
+ while (parameterIndexMatcher.find()) {
+
+ String parameterIndexString = parameterIndexMatcher.group(1);
+ Integer parameterIndex = getParameterIndex(parameterIndexString);
+ if (parameterIndex != null)
+ greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
+ }
+
+ return greatestParameterIndex;
+ }
+
+ /** */
+ private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
+
+ bindings.stream() //
+ .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) //
+ .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
+
+ if (!bindings.contains(binding))
+ bindings.add(binding);
+ }
+
+ /**
+ * An enum for the different types of bindings.
+ *
+ * @author Thomas Darimont
+ * @author Oliver Gierke
+ */
+ private enum ParameterBindingType {
+ // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
+ // character, while = does not.
+ /** */
+ LIKE("like "),
+
+ /** */
+ IN("in "),
+
+ /** */
+ AS_IS(null);
+
+ /** */
+ @Nullable
+ private final String keyword;
+
+ /** */
+ ParameterBindingType(@Nullable String keyword) {
+ this.keyword = keyword;
+ }
+
+ /**
+ * Returns the keyword that will tirgger the binding type or {@literal null} if the type is not triggered by
+ * a keyword.
+ *
+ * @return the keyword
+ */
+ @Nullable
+ public String getKeyword() {
+ return keyword;
+ }
+
+ /**
+ * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal
+ * #AS_IS} in case no other {@link ParameterBindingType} could be found.
+ */
+ static ParameterBindingType of(String typeSource) {
+ if (!StringUtils.hasText(typeSource))
+ return AS_IS;
+
+ for (ParameterBindingType type : values()) {
+ if (type.name().equalsIgnoreCase(typeSource.trim()))
+ return type;
+ }
+
+ throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
+ }
+ }
+ }
+
+ /**
+ * A generic parameter binding with name or position information.
+ *
+ * @author Thomas Darimont
+ */
+ static class ParameterBinding {
+ /** */
+ @Nullable
+ private final String name;
+
+ /** */
+ @Nullable
+ private final String expression;
+
+ /** */
+ @Nullable
+ private final Integer position;
+
+ /**
+ * Creates a new {@link ParameterBinding} for the parameter with the given position.
+ *
+ * @param position must not be {@literal null}.
+ */
+ ParameterBinding(Integer position) {
+ this(null, position, null);
+ }
+
+ /**
+ * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
+ * information. Either {@literal name} or {@literal position} must be not {@literal null}.
+ *
+ * @param name of the parameter may be {@literal null}.
+ * @param position of the parameter may be {@literal null}.
+ * @param expression the expression to apply to any value for this parameter.
+ */
+ ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) {
+
+ if (name == null)
+ Assert.notNull(position, "Position must not be null!");
+
+ if (position == null)
+ Assert.notNull(name, "Name must not be null!");
+
+ this.name = name;
+ this.position = position;
+ this.expression = expression;
+ }
+
+ /**
+ * Returns whether the binding has the given name. Will always be {@literal false} in case the {@link
+ * ParameterBinding} has been set up from a position.
+ */
+ boolean hasName(@Nullable String name) {
+ return position == null && this.name != null && this.name.equals(name);
+ }
+
+ /**
+ * Returns whether the binding has the given position. Will always be {@literal false} in case the {@link
+ * ParameterBinding} has been set up from a name.
+ */
+ boolean hasPosition(@Nullable Integer position) {
+ return position != null && name == null && position.equals(this.position);
+ }
+
+ /**
+ * @return the name
+ */
+ @Nullable
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @return the name
+ * @throws IllegalStateException if the name is not available.
+ */
+ String getRequiredName() throws IllegalStateException {
+
+ String name = getName();
+
+ if (name != null)
+ return name;
+
+ throw new IllegalStateException(String.format("Required name for %s not available!", this));
+ }
+
+ /**
+ * @return the position
+ */
+ @Nullable
+ Integer getPosition() {
+ return position;
+ }
+
+ /**
+ * @return the position
+ * @throws IllegalStateException if the position is not available.
+ */
+ int getRequiredPosition() throws IllegalStateException {
+
+ Integer position = getPosition();
+
+ if (position != null)
+ return position;
+
+ throw new IllegalStateException(String.format("Required position for %s not available!", this));
+ }
+
+ /**
+ * @return {@literal true} if this parameter binding is a synthetic SpEL expression.
+ */
+ public boolean isExpression() {
+ return expression != null;
+ }
+
+ /** */
+ @Override public int hashCode() {
+
+ int result = 17;
+
+ result += nullSafeHashCode(name);
+ result += nullSafeHashCode(position);
+ result += nullSafeHashCode(expression);
+
+ return result;
+ }
+
+ /** */
+ @Override public boolean equals(Object obj) {
+
+ if (!(obj instanceof ParameterBinding))
+ return false;
+
+ ParameterBinding that = (ParameterBinding)obj;
+
+ return nullSafeEquals(name, that.name) && nullSafeEquals(position, that.position)
+ && nullSafeEquals(expression, that.expression);
+ }
+
+ /** */
+ @Override public String toString() {
+ return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
+ getExpression());
+ }
+
+ /**
+ * @param valueToBind value to prepare
+ */
+ @Nullable
+ public Object prepare(@Nullable Object valueToBind) {
+ return valueToBind;
+ }
+
+ /** */
+ @Nullable
+ public String getExpression() {
+ return expression;
+ }
+ }
+
+ /**
+ * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as
+ * an {@code IN} parameter.
+ *
+ * @author Thomas Darimont
+ */
+ static class InParameterBinding extends ParameterBinding {
+ /**
+ * Creates a new {@link InParameterBinding} for the parameter with the given name.
+ */
+ InParameterBinding(String name, @Nullable String expression) {
+ super(name, null, expression);
+ }
+
+ /**
+ * Creates a new {@link InParameterBinding} for the parameter with the given position.
+ */
+ InParameterBinding(int position, @Nullable String expression) {
+ super(null, position, expression);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object)
+ */
+ @Override public Object prepare(@Nullable Object value) {
+ if (!ObjectUtils.isArray(value))
+ return value;
+
+ int length = Array.getLength(value);
+ Collection<Object> result = new ArrayList<>(length);
+
+ for (int i = 0; i < length; i++)
+ result.add(Array.get(value, i));
+
+ return result;
+ }
+
+ }
+
+ /**
+ * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE
+ * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA.
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ */
+ static class LikeParameterBinding extends ParameterBinding {
+ /** */
+ private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
+ Type.ENDING_WITH, Type.LIKE);
+
+ /** */
+ private final Type type;
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param type must not be {@literal null}.
+ */
+ LikeParameterBinding(String name, Type type) {
+ this(name, type, null);
+ }
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and
+ * parameter binding input.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param type must not be {@literal null}.
+ * @param expression may be {@literal null}.
+ */
+ LikeParameterBinding(String name, Type type, @Nullable String expression) {
+
+ super(name, null, expression);
+
+ Assert.hasText(name, "Name must not be null or empty!");
+ Assert.notNull(type, "Type must not be null!");
+
+ Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+ StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+ this.type = type;
+ }
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+ *
+ * @param position position of the parameter in the query.
+ * @param type must not be {@literal null}.
+ */
+ LikeParameterBinding(int position, Type type) {
+ this(position, type, null);
+ }
+
+ /**
+ * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+ *
+ * @param position position of the parameter in the query.
+ * @param type must not be {@literal null}.
+ * @param expression may be {@literal null}.
+ */
+ LikeParameterBinding(int position, Type type, @Nullable String expression) {
+
+ super(null, position, expression);
+
+ Assert.isTrue(position > 0, "Position must be greater than zero!");
+ Assert.notNull(type, "Type must not be null!");
+
+ Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+ StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+ this.type = type;
+ }
+
+ /**
+ * Returns the {@link Type} of the binding.
+ *
+ * @return the type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Prepares the given raw keyword according to the like type.
+ */
+ @Nullable
+ @Override public Object prepare(@Nullable Object value) {
+ if (value == null)
+ return null;
+
+ switch (type) {
+ case STARTING_WITH:
+ return String.format("%s%%", value.toString());
+ case ENDING_WITH:
+ return String.format("%%%s", value.toString());
+ case CONTAINING:
+ return String.format("%%%s%%", value.toString());
+ case LIKE:
+ default:
+ return value;
+ }
+ }
+
+ /** */
+ @Override public boolean equals(Object obj) {
+ if (!(obj instanceof LikeParameterBinding))
+ return false;
+
+ LikeParameterBinding that = (LikeParameterBinding)obj;
+
+ return super.equals(obj) && type.equals(that.type);
+ }
+
+ /** */
+ @Override public int hashCode() {
+
+ int result = super.hashCode();
+
+ result += nullSafeHashCode(type);
+
+ return result;
+ }
+
+ /** */
+ @Override public String toString() {
+ return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
+ }
+
+ /**
+ * Extracts the like {@link Type} from the given JPA like expression.
+ *
+ * @param expression must not be {@literal null} or empty.
+ */
+ private static Type getLikeTypeFrom(String expression) {
+
+ Assert.hasText(expression, "Expression must not be null or empty!");
+
+ if (expression.matches("%.*%"))
+ return Type.CONTAINING;
+
+ if (expression.startsWith("%"))
+ return Type.ENDING_WITH;
+
+ if (expression.endsWith("%"))
+ return Type.STARTING_WITH;
+
+ return Type.LIKE;
+ }
+
+ }
+
+ /** */
+ static class Metadata {
+ /**
+ * Uses jdbc style parameters.
+ */
+ private boolean usesJdbcStyleParameters;
+ }
+
+ /**
+ * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers
+ * an API to query that information.
+ *
+ * @author Jens Schauder
+ * @author Oliver Gierke
+ */
+ static class QuotationMap {
+ /** */
+ private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
+
+ /** */
+ private final List<Range<Integer>> quotedRanges = new ArrayList<>();
+
+ /**
+ * Creates a new instance for the query.
+ *
+ * @param query can be {@literal null}.
+ */
+ public QuotationMap(@Nullable String query) {
+ if (query == null)
+ return;
+
+ Character inQuotation = null;
+ int start = 0;
+
+ for (int i = 0; i < query.length(); i++) {
+ char currentChar = query.charAt(i);
+
+ if (QUOTING_CHARACTERS.contains(currentChar)) {
+ if (inQuotation == null) {
+
+ inQuotation = currentChar;
+ start = i;
+ }
+ else if (currentChar == inQuotation) {
+ inQuotation = null;
+
+ quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
+ }
+ }
+ }
+
+ if (inQuotation != null) {
+ throw new IllegalArgumentException(
+ String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start));
+ }
+ }
+
+ /**
+ * Checks if a given index is within a quoted range.
+ *
+ * @param idx to check if it is part of a quoted range.
+ * @return whether the query contains a quoted range at {@literal index}.
+ */
+ public boolean isQuoted(int idx) {
+ return quotedRanges.stream().anyMatch(r -> r.contains(idx));
+ }
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/package-info.java
new file mode 100644
index 0000000..cfa3801
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/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. -->
+ * Package includes classes that integrates with Apache Ignite SQL engine.
+ */
+package org.apache.ignite.springdata22.repository.query;
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/ConditionFalse.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/ConditionFalse.java
new file mode 100644
index 0000000..a69dc8e
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/ConditionFalse.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.springdata22.repository.support;
+
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+/**
+ * Always false condition. Tells spring context never load bean with such Condition.
+ */
+public class ConditionFalse implements Condition {
+ /** {@inheritDoc} */
+ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ return false;
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactory.java
new file mode 100644
index 0000000..97d31e9
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactory.java
@@ -0,0 +1,274 @@
+/*
+ * 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.springdata22.repository.support;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.apache.ignite.springdata22.repository.query.IgniteQuery;
+import org.apache.ignite.springdata22.repository.query.IgniteQueryGenerator;
+import org.apache.ignite.springdata22.repository.query.IgniteRepositoryQuery;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanExpressionContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.expression.StandardBeanExpressionResolver;
+import org.springframework.data.repository.core.EntityInformation;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractEntityInformation;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Crucial for spring-data functionality class. Create proxies for repositories.
+ * <p>
+ * Supports multiple Ignite Instances on same JVM.
+ * <p>
+ * This is pretty useful working with Spring repositories bound to different Ignite intances within same application.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteRepositoryFactory extends RepositoryFactorySupport {
+ /** Spring application context */
+ private final ApplicationContext ctx;
+
+ /** Spring application bean factory */
+ private final DefaultListableBeanFactory beanFactory;
+
+ /** Spring application expression resolver */
+ private final StandardBeanExpressionResolver resolver = new StandardBeanExpressionResolver();
+
+ /** Spring application bean expression context */
+ private final BeanExpressionContext beanExpressionContext;
+
+ /** Mapping of a repository to a cache. */
+ private final Map<Class<?>, String> repoToCache = new HashMap<>();
+
+ /** Mapping of a repository to a ignite instance. */
+ private final Map<Class<?>, Ignite> repoToIgnite = new HashMap<>();
+
+ /**
+ * Creates the factory with initialized {@link Ignite} instance.
+ *
+ * @param ctx the ctx
+ */
+ public IgniteRepositoryFactory(ApplicationContext ctx) {
+ this.ctx = ctx;
+
+ beanFactory = new DefaultListableBeanFactory(ctx.getAutowireCapableBeanFactory());
+
+ beanExpressionContext = new BeanExpressionContext(beanFactory, null);
+ }
+
+ /** */
+ private Ignite igniteForRepoConfig(RepositoryConfig config) {
+ try {
+ String igniteInstanceName = evaluateExpression(config.igniteInstance());
+ return (Ignite)ctx.getBean(igniteInstanceName);
+ }
+ catch (BeansException ex) {
+ try {
+ String igniteConfigName = evaluateExpression(config.igniteCfg());
+ IgniteConfiguration cfg = (IgniteConfiguration)ctx.getBean(igniteConfigName);
+ try {
+ // first try to attach to existing ignite instance
+ return Ignition.ignite(cfg.getIgniteInstanceName());
+ }
+ catch (Exception ignored) {
+ // nop
+ }
+ return Ignition.start(cfg);
+ }
+ catch (BeansException ex2) {
+ try {
+ String igniteSpringCfgPath = evaluateExpression(config.igniteSpringCfgPath());
+ String path = (String)ctx.getBean(igniteSpringCfgPath);
+ return Ignition.start(path);
+ }
+ catch (BeansException ex3) {
+ throw new IgniteException("Failed to initialize Ignite repository factory. Ignite instance or"
+ + " IgniteConfiguration or a path to Ignite's spring XML "
+ + "configuration must be defined in the"
+ + " application configuration");
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
+ return new AbstractEntityInformation<T, ID>(domainClass) {
+ /** {@inheritDoc} */
+ @Override public ID getId(T entity) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Class<ID> getIdType() {
+ return null;
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
+ return IgniteRepositoryImpl.class;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected synchronized RepositoryMetadata getRepositoryMetadata(Class<?> repoItf) {
+ Assert.notNull(repoItf, "Repository interface must be set.");
+ Assert.isAssignable(IgniteRepository.class, repoItf, "Repository must implement IgniteRepository interface.");
+
+ RepositoryConfig annotation = repoItf.getAnnotation(RepositoryConfig.class);
+
+ Assert.notNull(annotation, "Set a name of an Apache Ignite cache using @RepositoryConfig annotation to map "
+ + "this repository to the underlying cache.");
+
+ Assert.hasText(annotation.cacheName(), "Set a name of an Apache Ignite cache using @RepositoryConfig "
+ + "annotation to map this repository to the underlying cache.");
+
+ String cacheName = evaluateExpression(annotation.cacheName());
+
+ repoToCache.put(repoItf, cacheName);
+
+ repoToIgnite.put(repoItf, igniteForRepoConfig(annotation));
+
+ return super.getRepositoryMetadata(repoItf);
+ }
+
+ /**
+ * Evaluate the SpEL expression
+ *
+ * @param spelExpression SpEL expression
+ * @return the result of execution of the SpEL expression
+ */
+ private String evaluateExpression(String spelExpression) {
+ return (String)resolver.evaluate(spelExpression, beanExpressionContext);
+ }
+
+ /** Control underlying cache creation to avoid cache creation by mistake */
+ private IgniteCache getRepositoryCache(Class<?> repoIf) {
+ Ignite ignite = repoToIgnite.get(repoIf);
+
+ RepositoryConfig config = repoIf.getAnnotation(RepositoryConfig.class);
+
+ String cacheName = repoToCache.get(repoIf);
+
+ IgniteCache c = config.autoCreateCache() ? ignite.getOrCreateCache(cacheName) : ignite.cache(cacheName);
+
+ if (c == null) {
+ throw new IllegalStateException(
+ "Cache '" + cacheName + "' not found for repository interface " + repoIf.getName()
+ + ". Please, add a cache configuration to ignite configuration"
+ + " or pass autoCreateCache=true to org.apache.ignite.springdata22"
+ + ".repository.config.RepositoryConfig annotation.");
+ }
+
+ return c;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Object getTargetRepository(RepositoryInformation metadata) {
+ Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+ return getTargetRepositoryViaReflection(metadata, ignite,
+ getRepositoryCache(metadata.getRepositoryInterface()));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(final QueryLookupStrategy.Key key,
+ QueryMethodEvaluationContextProvider evaluationContextProvider) {
+ return Optional.of((mtd, metadata, factory, namedQueries) -> {
+ final Query annotation = mtd.getAnnotation(Query.class);
+ final Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+ if (annotation != null && (StringUtils.hasText(annotation.value()) || annotation.textQuery() || annotation
+ .dynamicQuery())) {
+
+ String qryStr = annotation.value();
+
+ boolean annotatedIgniteQuery = !annotation.dynamicQuery() && (StringUtils.hasText(qryStr) || annotation
+ .textQuery());
+
+ IgniteQuery query = annotatedIgniteQuery ? new IgniteQuery(qryStr,
+ !annotation.textQuery() && (isFieldQuery(qryStr) || annotation.forceFieldsQuery()),
+ annotation.textQuery(), false, IgniteQueryGenerator.getOptions(mtd)) : null;
+
+ if (key != QueryLookupStrategy.Key.CREATE) {
+ return new IgniteRepositoryQuery(ignite, metadata, query, mtd, factory,
+ getRepositoryCache(metadata.getRepositoryInterface()),
+ annotatedIgniteQuery ? DynamicQueryConfig.fromQueryAnnotation(annotation) : null,
+ evaluationContextProvider);
+ }
+ }
+
+ if (key == QueryLookupStrategy.Key.USE_DECLARED_QUERY) {
+ throw new IllegalStateException("To use QueryLookupStrategy.Key.USE_DECLARED_QUERY, pass "
+ + "a query string via org.apache.ignite.springdata22.repository"
+ + ".config.Query annotation.");
+ }
+
+ return new IgniteRepositoryQuery(ignite, metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd,
+ factory, getRepositoryCache(metadata.getRepositoryInterface()),
+ DynamicQueryConfig.fromQueryAnnotation(annotation), evaluationContextProvider);
+ });
+ }
+
+ /**
+ * @param qry Query string.
+ * @return {@code true} if query is SqlFieldsQuery.
+ */
+ public static boolean isFieldQuery(String qry) {
+ String qryUpperCase = qry.toUpperCase();
+
+ return isStatement(qryUpperCase) && !qryUpperCase.matches("^SELECT\\s+(?:\\w+\\.)?+\\*.*");
+ }
+
+ /**
+ * Evaluates if the query starts with a clause.<br>
+ * <code>SELECT, INSERT, UPDATE, MERGE, DELETE</code>
+ *
+ * @param qryUpperCase Query string in upper case.
+ * @return {@code true} if query is full SQL statement.
+ */
+ private static boolean isStatement(String qryUpperCase) {
+ return qryUpperCase.matches("^\\s*SELECT\\b.*") ||
+ // update
+ qryUpperCase.matches("^\\s*UPDATE\\b.*") ||
+ // delete
+ qryUpperCase.matches("^\\s*DELETE\\b.*") ||
+ // merge
+ qryUpperCase.matches("^\\s*MERGE\\b.*") ||
+ // insert
+ qryUpperCase.matches("^\\s*INSERT\\b.*");
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactoryBean.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactoryBean.java
new file mode 100644
index 0000000..ece9f31
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactoryBean.java
@@ -0,0 +1,67 @@
+/*
+ * 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.springdata22.repository.support;
+
+import java.io.Serializable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Apache Ignite repository factory bean.
+ * <p>
+ * The {@link org.apache.ignite.springdata22.repository.config.RepositoryConfig} requires to define one of the
+ * parameters below in your Spring application configuration in order to get an access to Apache Ignite cluster:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance" by default</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg" by default</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath" by default</li>
+ * <ul/>
+ *
+ * @param <T> Repository type, {@link IgniteRepository}
+ * @param <V> Domain object class.
+ * @param <K> Domain object key, super expects {@link Serializable}.
+ */
+public class IgniteRepositoryFactoryBean<T extends Repository<V, K>, V, K extends Serializable>
+ extends RepositoryFactoryBeanSupport<T, V, K> implements ApplicationContextAware {
+ /** */
+ private ApplicationContext ctx;
+
+ /**
+ * @param repoInterface Repository interface.
+ */
+ protected IgniteRepositoryFactoryBean(Class<? extends T> repoInterface) {
+ super(repoInterface);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void setApplicationContext(ApplicationContext ctx) throws BeansException {
+ this.ctx = ctx;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryFactorySupport createRepositoryFactory() {
+ return new IgniteRepositoryFactory(ctx);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryImpl.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryImpl.java
new file mode 100644
index 0000000..c4d9834
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryImpl.java
@@ -0,0 +1,221 @@
+/*
+ * 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.springdata22.repository.support;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.cache.Cache;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * General Apache Ignite repository implementation. This bean should've never been loaded by context directly, only via
+ * {@link IgniteRepositoryFactory}
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Conditional(ConditionFalse.class)
+public class IgniteRepositoryImpl<V, K extends Serializable> implements IgniteRepository<V, K> {
+ /**
+ * Ignite Cache bound to the repository
+ */
+ private final IgniteCache<K, V> cache;
+
+ /**
+ * Ignite instance bound to the repository
+ */
+ private final Ignite ignite;
+
+ /**
+ * Repository constructor.
+ *
+ * @param ignite the ignite
+ * @param cache Initialized cache instance.
+ */
+ public IgniteRepositoryImpl(Ignite ignite, IgniteCache<K, V> cache) {
+ this.cache = cache;
+ this.ignite = ignite;
+ }
+
+ /** {@inheritDoc} */
+ @Override public IgniteCache<K, V> cache() {
+ return cache;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Ignite ignite() {
+ return ignite;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> S save(K key, S entity) {
+ cache.put(key, entity);
+
+ return entity;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> Iterable<S> save(Map<K, S> entities) {
+ cache.putAll(entities);
+
+ return entities.values();
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc) {
+ if (expiryPlc != null)
+ cache.withExpiryPolicy(expiryPlc).put(key, entity);
+ else
+ cache.put(key, entity);
+ return entity;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc) {
+ if (expiryPlc != null)
+ cache.withExpiryPolicy(expiryPlc).putAll(entities);
+ else
+ cache.putAll(entities);
+ return entities.values();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override public <S extends V> S save(S entity) {
+ throw new UnsupportedOperationException("Use IgniteRepository.save(key,value) method instead.");
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override public <S extends V> Iterable<S> saveAll(Iterable<S> entities) {
+ throw new UnsupportedOperationException("Use IgniteRepository.save(Map<keys,value>) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public Optional<V> findById(K id) {
+ return Optional.ofNullable(cache.get(id));
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean existsById(K id) {
+ return cache.containsKey(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterable<V> findAll() {
+ final Iterator<Cache.Entry<K, V>> iter = cache.iterator();
+
+ return new Iterable<V>() {
+ /** */
+ @Override public Iterator<V> iterator() {
+ return new Iterator<V>() {
+ /** {@inheritDoc} */
+ @Override public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ /** {@inheritDoc} */
+ @Override public V next() {
+ return iter.next().getValue();
+ }
+
+ /** {@inheritDoc} */
+ @Override public void remove() {
+ iter.remove();
+ }
+ };
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterable<V> findAllById(Iterable<K> ids) {
+ if (ids instanceof Set)
+ return cache.getAll((Set<K>)ids).values();
+
+ if (ids instanceof Collection)
+ return cache.getAll(new HashSet<>((Collection<K>)ids)).values();
+
+ TreeSet<K> keys = new TreeSet<>();
+
+ for (K id : ids)
+ keys.add(id);
+
+ return cache.getAll(keys).values();
+ }
+
+ /** {@inheritDoc} */
+ @Override public long count() {
+ return cache.size(CachePeekMode.PRIMARY);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteById(K id) {
+ cache.remove(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(V entity) {
+ throw new UnsupportedOperationException("Use IgniteRepository.deleteById(key) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAll(Iterable<? extends V> entities) {
+ throw new UnsupportedOperationException("Use IgniteRepository.deleteAllById(keys) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAllById(Iterable<K> ids) {
+ if (ids instanceof Set) {
+ cache.removeAll((Set<K>)ids);
+ return;
+ }
+
+ if (ids instanceof Collection) {
+ cache.removeAll(new HashSet<>((Collection<K>)ids));
+ return;
+ }
+
+ TreeSet<K> keys = new TreeSet<>();
+
+ for (K id : ids)
+ keys.add(id);
+
+ cache.removeAll(keys);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAll() {
+ cache.clear();
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/package-info.java
new file mode 100644
index 0000000..cc15fc7
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/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. -->
+ * Package contains supporting files required by Spring Data framework.
+ */
+package org.apache.ignite.springdata22.repository.support;
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
new file mode 100644
index 0000000..c4f9c0e
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.springdata;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.compoundkey.City;
+import org.apache.ignite.springdata.compoundkey.CityKey;
+import org.apache.ignite.springdata.compoundkey.CityRepository;
+import org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * Test with using conpoud key in spring-data
+ * */
+public class IgniteSpringDataCompoundKeyTest extends GridCommonAbstractTest {
+ /** Application context */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** City repository */
+ private static CityRepository repo;
+
+ /** Cache name */
+ private static final String CACHE_NAME = "City";
+
+ /** Cities count */
+ private static final int TOTAL_COUNT = 5;
+
+ /** Count Afganistan cities */
+ private static final int AFG_COUNT = 4;
+
+ /** Kabul identifier */
+ private static final int KABUL_ID = 1;
+
+ /** Quandahar identifier */
+ private static final int QUANDAHAR_ID = 2;
+
+ /** Afganistan county code */
+ private static final String AFG = "AFG";
+
+ /** test city Kabul */
+ private static final City KABUL = new City("Kabul", "Kabol", 1780000);
+
+ /** test city Quandahar */
+ private static final City QUANDAHAR = new City("Qandahar","Qandahar", 237500);
+
+ /**
+ * Performs context initialization before tests.
+ */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+ ctx.register(CompoundKeyApplicationConfiguration.class);
+ ctx.refresh();
+
+ repo = ctx.getBean(CityRepository.class);
+ }
+
+ /**
+ * Load data
+ * */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ loadData();
+
+ assertEquals(TOTAL_COUNT, repo.count());
+ }
+
+ /**
+ * Performs context destroy after tests.
+ */
+ @Override protected void afterTestsStopped() {
+ ctx.close();
+ }
+
+ /** load data*/
+ public void loadData() throws Exception {
+ Ignite ignite = ctx.getBean(Ignite.class);
+
+ if (ignite.cacheNames().contains(CACHE_NAME))
+ ignite.destroyCache(CACHE_NAME);
+
+ try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:" + CompoundKeyApplicationConfiguration.CLI_CONN_PORT + '/')) {
+ Statement st = conn.createStatement();
+
+ st.execute("DROP TABLE IF EXISTS City");
+ st.execute("CREATE TABLE City (ID INT, Name VARCHAR, CountryCode CHAR(3), District VARCHAR, Population INT, PRIMARY KEY (ID, CountryCode)) WITH \"template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, KEY_TYPE=org.apache.ignite.springdata.compoundkey.CityKey, VALUE_TYPE=org.apache.ignite.springdata.compoundkey.City\"");
+ st.execute("SET STREAMING ON;");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (1,'Kabul','AFG','Kabol',1780000)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (2,'Qandahar','AFG','Qandahar',237500)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (3,'Herat','AFG','Herat',186800)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (4,'Mazar-e-Sharif','AFG','Balkh',127800)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (5,'Amsterdam','NLD','Noord-Holland',731200)");
+ }
+ }
+
+ /** Test */
+ @Test
+ public void test() {
+ assertEquals(Optional.of(KABUL), repo.findById(new CityKey(KABUL_ID, AFG)));
+ assertEquals(AFG_COUNT, repo.findByCountryCode(AFG).size());
+ assertEquals(QUANDAHAR, repo.findById(QUANDAHAR_ID));
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
new file mode 100644
index 0000000..d9de9b7
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.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.springdata;
+
+import java.util.Collection;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonExpressionRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * Test with using repository which is configured by Spring EL
+ */
+public class IgniteSpringDataCrudSelfExpressionTest extends GridCommonAbstractTest {
+ /** Number of entries to store */
+ private static final int CACHE_SIZE = 1000;
+
+ /** Repository. */
+ private static PersonExpressionRepository repo;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** */
+ @Rule
+ public final ExpectedException expected = ExpectedException.none();
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+ ctx.register(ApplicationConfiguration.class);
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonExpressionRepository.class);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ fillInRepository();
+
+ assertEquals(CACHE_SIZE, repo.count());
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+
+ super.afterTest();
+ }
+
+ /** */
+ private void fillInRepository() {
+ for (int i = 0; i < CACHE_SIZE - 5; i++) {
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+
+ repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() {
+ ctx.close();
+ }
+
+ /**
+ * Tests put & get operations.
+ */
+ @Test
+ public void testPutGet() {
+ Person person = new Person("some_name", "some_surname");
+
+ int id = CACHE_SIZE + 1;
+
+ assertEquals(person, repo.save(id, person));
+
+ assertTrue(repo.existsById(id));
+
+ assertEquals(person, repo.findById(id).get());
+
+ expected.expect(UnsupportedOperationException.class);
+ expected.expectMessage("Use IgniteRepository.save(key,value) method instead.");
+ repo.save(person);
+ }
+
+ /**
+ * Tests SpEL expression.
+ */
+ @Test
+ public void testCacheCount() {
+ Ignite ignite = ctx.getBean("igniteInstance", Ignite.class);
+
+ Collection<String> cacheNames = ignite.cacheNames();
+
+ assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+ cacheNames.contains("#{cacheNames.personCacheName}"));
+
+ assertTrue("Cache \"PersonCache\" isn't found!",
+ cacheNames.contains("PersonCache"));
+ }
+
+ /** */
+ @Test
+ public void testCacheCountTWO() {
+ Ignite ignite = ctx.getBean("igniteInstanceTWO", Ignite.class);
+
+ Collection<String> cacheNames = ignite.cacheNames();
+
+ assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+ cacheNames.contains("#{cacheNames.personCacheName}"));
+
+ assertTrue("Cache \"PersonCache\" isn't found!",
+ cacheNames.contains("PersonCache"));
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
new file mode 100644
index 0000000..38785ef
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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.springdata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.TreeSet;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.FullNameProjection;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * CRUD tests.
+ */
+public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
+ /** Repository. */
+ private static PersonRepository repo;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** Number of entries to store */
+ private static int CACHE_SIZE = 1000;
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+
+ ctx.register(ApplicationConfiguration.class);
+
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonRepository.class);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ fillInRepository();
+
+ assertEquals(CACHE_SIZE, repo.count());
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+
+ super.afterTest();
+ }
+
+ /** */
+ private void fillInRepository() {
+ for (int i = 0; i < CACHE_SIZE - 5; i++) {
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+
+ repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() throws Exception {
+ ctx.destroy();
+ }
+
+ /** */
+ @Test
+ public void testPutGet() {
+ Person person = new Person("some_name", "some_surname");
+
+ int id = CACHE_SIZE + 1;
+
+ assertEquals(person, repo.save(id, person));
+
+ assertTrue(repo.existsById(id));
+
+ assertEquals(person, repo.findById(id).get());
+
+ try {
+ repo.save(person);
+
+ fail("Managed to save a Person without ID");
+ }
+ catch (UnsupportedOperationException e) {
+ //excepted
+ }
+ }
+
+ /** */
+ @Test
+ public void testPutAllGetAll() {
+ LinkedHashMap<Integer, Person> map = new LinkedHashMap<>();
+
+ for (int i = CACHE_SIZE; i < CACHE_SIZE + 50; i++)
+ map.put(i, new Person("some_name" + i, "some_surname" + i));
+
+ Iterator<Person> persons = repo.save(map).iterator();
+
+ assertEquals(CACHE_SIZE + 50, repo.count());
+
+ Iterator<Person> origPersons = map.values().iterator();
+
+ while (persons.hasNext())
+ assertEquals(origPersons.next(), persons.next());
+
+ try {
+ repo.saveAll(map.values());
+
+ fail("Managed to save a list of Persons with ids");
+ }
+ catch (UnsupportedOperationException e) {
+ //expected
+ }
+
+ persons = repo.findAllById(map.keySet()).iterator();
+
+ int counter = 0;
+
+ while (persons.hasNext()) {
+ persons.next();
+ counter++;
+ }
+
+ assertEquals(map.size(), counter);
+ }
+
+ /** */
+ @Test
+ public void testGetAll() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ Iterator<Person> persons = repo.findAll().iterator();
+
+ int counter = 0;
+
+ while (persons.hasNext()) {
+ persons.next();
+ counter++;
+ }
+
+ assertEquals(repo.count(), counter);
+ }
+
+ /** */
+ @Test
+ public void testDelete() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ repo.deleteById(0);
+
+ assertEquals(CACHE_SIZE - 1, repo.count());
+ assertEquals(Optional.empty(),repo.findById(0));
+
+ try {
+ repo.delete(new Person("", ""));
+
+ fail("Managed to delete a Person without id");
+ }
+ catch (UnsupportedOperationException e) {
+ //expected
+ }
+ }
+
+ /** */
+ @Test
+ public void testDeleteSet() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ TreeSet<Integer> ids = new TreeSet<>();
+
+ for (int i = 0; i < CACHE_SIZE / 2; i++)
+ ids.add(i);
+
+ repo.deleteAllById(ids);
+
+ assertEquals(CACHE_SIZE / 2, repo.count());
+
+ try {
+ ArrayList<Person> persons = new ArrayList<>();
+
+ for (int i = 0; i < 3; i++)
+ persons.add(new Person(String.valueOf(i), String.valueOf(i)));
+
+ repo.deleteAll(persons);
+
+ fail("Managed to delete Persons without ids");
+ }
+ catch (UnsupportedOperationException e) {
+ //expected
+ }
+ }
+
+ /** */
+ @Test
+ public void testDeleteAll() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+ }
+
+ /**
+ * Delete existing record.
+ */
+ @Test
+ public void testDeleteByFirstName() {
+ assertEquals(repo.countByFirstNameLike("uniquePerson"), 1);
+
+ long cnt = repo.deleteByFirstName("uniquePerson");
+
+ assertEquals(1, cnt);
+ }
+
+ /**
+ * Delete NON existing record.
+ */
+ @Test
+ public void testDeleteExpression() {
+ long cnt = repo.deleteByFirstName("880");
+
+ assertEquals(0, cnt);
+ }
+
+ /**
+ * Delete Multiple records due to where.
+ */
+ @Test
+ public void testDeleteExpressionMultiple() {
+ long count = repo.countByFirstName("nonUniquePerson");
+ long cnt = repo.deleteByFirstName("nonUniquePerson");
+
+ assertEquals(cnt, count);
+ }
+
+ /**
+ * Remove should do the same than Delete.
+ */
+ @Test
+ public void testRemoveExpression() {
+ repo.removeByFirstName("person3f");
+
+ long count = repo.count();
+ assertEquals(CACHE_SIZE - 1, count);
+ }
+
+ /**
+ * Delete unique record using lower case key word.
+ */
+ @Test
+ public void testDeleteQuery() {
+ repo.deleteBySecondNameLowerCase("uniqueLastName");
+
+ long countAfter = repo.count();
+ assertEquals(CACHE_SIZE - 1, countAfter);
+ }
+
+ /**
+ * Try to delete with a wrong @Query.
+ */
+ @Test
+ public void testWrongDeleteQuery() {
+ long countBefore = repo.countByFirstNameLike("person3f");
+
+ try {
+ repo.deleteWrongByFirstNameQuery("person3f");
+ }
+ catch (Exception e) {
+ //expected
+ }
+
+ long countAfter = repo.countByFirstNameLike("person3f");
+ assertEquals(countBefore, countAfter);
+ }
+
+ /**
+ * Update with a @Query a record.
+ */
+ @Test
+ public void testUpdateQueryMixedCase() {
+ final String newSecondName = "updatedUniqueSecondName";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<Person> person = repo.findByFirstName("uniquePerson");
+ assertEquals(person.get(0).getSecondName(), "updatedUniqueSecondName");
+ }
+
+ /**
+ * Update with a @Query a record
+ */
+ @Test
+ public void testUpdateQueryMixedCaseProjection() {
+ final String newSecondName = "updatedUniqueSecondName1";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjection("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName1");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionNamedParameter() {
+ final String newSecondName = "updatedUniqueSecondName2";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseDynamicProjectionNamedParameter() {
+ final String newSecondName = "updatedUniqueSecondName2";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+ List<FullNameProjection> personFullName = repo.queryByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+ assertEquals(personFullName.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryOneMixedCaseDynamicProjectionNamedParameter() {
+ final String newSecondName = "updatedUniqueSecondName2";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ PersonProjection person = repo.queryOneByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+ assertEquals(person.getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+ FullNameProjection personFullName = repo.queryOneByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+ assertEquals(personFullName.getFullName(), "uniquePerson updatedUniqueSecondName2");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionIndexedParameter() {
+ final String newSecondName = "updatedUniqueSecondName3";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedIndexedParameter("notUsed","uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName3");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionIndexedParameterLuceneTextQuery() {
+ final String newSecondName = "updatedUniqueSecondName4";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.textQueryByFirstNameWithProjectionNamedParameter("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName4");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionNamedParameterAndTemplateDomainEntityVariable() {
+ final String newSecondName = "updatedUniqueSecondName5";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName5");
+ }
+
+ /** */
+ @Test
+ public void testUpdateQueryMixedCaseProjectionNamedParameterWithSpELExtension() {
+ final String newSecondName = "updatedUniqueSecondName6";
+ int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+ assertEquals(1, cnt);
+
+ List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterWithSpELExtension("uniquePerson");
+ assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName6");
+ assertEquals(person.get(0).getFirstName(), person.get(0).getFirstNameTransformed());
+ }
+
+ /**
+ * Update with a wrong @Query
+ */
+ @Test
+ public void testWrongUpdateQuery() {
+ final String newSecondName = "updatedUniqueSecondName";
+ int rowsUpdated = 0;
+
+ try {
+ rowsUpdated = repo.setWrongFixedSecondName(newSecondName, "uniquePerson");
+ }
+ catch (Exception ignored) {
+ //expected
+ }
+
+ assertEquals(0, rowsUpdated);
+
+ List<Person> person = repo.findByFirstName("uniquePerson");
+ assertEquals(person.get(0).getSecondName(), "uniqueLastName");
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
new file mode 100644
index 0000000..d35c781
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
@@ -0,0 +1,409 @@
+/*
+ * 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.springdata;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonRepositoryOtherIgniteInstance;
+import org.apache.ignite.springdata.misc.PersonSecondRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+/**
+ *
+ */
+public class IgniteSpringDataQueriesSelfTest extends GridCommonAbstractTest {
+ /** Repository. */
+ private static PersonRepository repo;
+
+ /** Repository 2. */
+ private static PersonSecondRepository repo2;
+
+ /**
+ * Repository Ignite Instance cluster TWO.
+ */
+ private static PersonRepositoryOtherIgniteInstance repoTWO;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /**
+ * Number of entries to store
+ */
+ private static int CACHE_SIZE = 1000;
+
+ /**
+ * Performs context initialization before tests.
+ */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+
+ ctx.register(ApplicationConfiguration.class);
+
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonRepository.class);
+ repo2 = ctx.getBean(PersonSecondRepository.class);
+ // repository on another ignite instance (and another cluster)
+ repoTWO = ctx.getBean(PersonRepositoryOtherIgniteInstance.class);
+
+ for (int i = 0; i < CACHE_SIZE; i++) {
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ repoTWO.save(i, new Person("TWOperson" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+ }
+
+ /**
+ * Performs context destroy after tests.
+ */
+ @Override protected void afterTestsStopped() throws Exception {
+ ctx.destroy();
+ }
+
+ /** */
+ @Test
+ public void testExplicitQuery() {
+ List<Person> persons = repo.simpleQuery("person4a");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("person4a", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testExplicitQueryTWO() {
+ List<Person> persons = repoTWO.simpleQuery("TWOperson4a");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("TWOperson4a", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testEqualsPart() {
+ List<Person> persons = repo.findByFirstName("person4e");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("person4e", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testEqualsPartTWO() {
+ List<Person> persons = repoTWO.findByFirstName("TWOperson4e");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("TWOperson4e", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testContainingPart() {
+ List<Person> persons = repo.findByFirstNameContaining("person4");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertTrue(person.getFirstName().startsWith("person4"));
+ }
+
+ /** */
+ @Test
+ public void testContainingPartTWO() {
+ List<Person> persons = repoTWO.findByFirstNameContaining("TWOperson4");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertTrue(person.getFirstName().startsWith("TWOperson4"));
+ }
+
+ /** */
+ @Test
+ public void testTopPart() {
+ Iterable<Person> top = repo.findTopByFirstNameContaining("person4");
+
+ Iterator<Person> iter = top.iterator();
+
+ Person person = iter.next();
+
+ assertFalse(iter.hasNext());
+
+ assertTrue(person.getFirstName().startsWith("person4"));
+ }
+
+ /** */
+ @Test
+ public void testTopPartTWO() {
+ Iterable<Person> top = repoTWO.findTopByFirstNameContaining("TWOperson4");
+
+ Iterator<Person> iter = top.iterator();
+
+ Person person = iter.next();
+
+ assertFalse(iter.hasNext());
+
+ assertTrue(person.getFirstName().startsWith("TWOperson4"));
+ }
+
+ /** */
+ @Test
+ public void testLikeAndLimit() {
+ Iterable<Person> like = repo.findFirst10ByFirstNameLike("person");
+
+ int cnt = 0;
+
+ for (Person next : like) {
+ assertTrue(next.getFirstName().contains("person"));
+
+ cnt++;
+ }
+
+ assertEquals(10, cnt);
+ }
+
+ /** */
+ @Test
+ public void testLikeAndLimitTWO() {
+ Iterable<Person> like = repoTWO.findFirst10ByFirstNameLike("TWOperson");
+
+ int cnt = 0;
+
+ for (Person next : like) {
+ assertTrue(next.getFirstName().contains("TWOperson"));
+
+ cnt++;
+ }
+
+ assertEquals(10, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCount() {
+ int cnt = repo.countByFirstNameLike("person");
+
+ assertEquals(1000, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCountTWO() {
+ int cnt = repoTWO.countByFirstNameLike("TWOperson");
+
+ assertEquals(1000, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCount2() {
+ int cnt = repo.countByFirstNameLike("person4");
+
+ assertTrue(cnt < 1000);
+ }
+
+ /** */
+ @Test
+ public void testCount2TWO() {
+ int cnt = repoTWO.countByFirstNameLike("TWOperson4");
+
+ assertTrue(cnt < 1000);
+ }
+
+ /** */
+ @Test
+ public void testPageable() {
+ PageRequest pageable = PageRequest.of(1, 5, Sort.Direction.DESC, "firstName");
+
+ HashSet<String> firstNames = new HashSet<>();
+
+ List<Person> pageable1 = repo.findByFirstNameRegex("^[a-z]+$", pageable);
+
+ assertEquals(5, pageable1.size());
+
+ for (Person person : pageable1) {
+ firstNames.add(person.getFirstName());
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+ }
+
+ List<Person> pageable2 = repo.findByFirstNameRegex("^[a-z]+$", pageable.next());
+
+ assertEquals(5, pageable2.size());
+
+ for (Person person : pageable2) {
+ firstNames.add(person.getFirstName());
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+ }
+
+ assertEquals(10, firstNames.size());
+ }
+
+ /** */
+ @Test
+ public void testAndAndOr() {
+ int cntAnd = repo.countByFirstNameLikeAndSecondNameLike("person1", "lastName1");
+
+ int cntOr = repo.countByFirstNameStartingWithOrSecondNameStartingWith("person1", "lastName1");
+
+ assertTrue(cntAnd <= cntOr);
+ }
+
+ /** */
+ @Test
+ public void testQueryWithSort() {
+ List<Person> persons = repo.queryWithSort("^[a-z]+$", Sort.by(Sort.Direction.DESC, "secondName"));
+
+ Person previous = persons.get(0);
+
+ for (Person person : persons) {
+ assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+ previous = person;
+ }
+ }
+
+ /** */
+ @Test
+ public void testQueryWithPaging() {
+ List<Person> persons = repo.queryWithPageable("^[a-z]+$", PageRequest.of(1, 7, Sort.Direction.DESC, "secondName"));
+
+ assertEquals(7, persons.size());
+
+ Person previous = persons.get(0);
+
+ for (Person person : persons) {
+ assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+ previous = person;
+ }
+ }
+
+ /** */
+ @Test
+ public void testQueryFields() {
+ List<String> persons = repo.selectField("^[a-z]+$", PageRequest.of(1, 7, Sort.Direction.DESC, "secondName"));
+
+ assertEquals(7, persons.size());
+ }
+
+ /** */
+ @Test
+ public void testFindCacheEntries() {
+ List<Cache.Entry<Integer, Person>> cacheEntries = repo.findBySecondNameLike("stName1");
+
+ assertFalse(cacheEntries.isEmpty());
+
+ for (Cache.Entry<Integer, Person> entry : cacheEntries)
+ assertTrue(entry.getValue().getSecondName().contains("stName1"));
+ }
+
+ /** */
+ @Test
+ public void testFindOneCacheEntry() {
+ Cache.Entry<Integer, Person> cacheEntry = repo.findTopBySecondNameLike("tName18");
+
+ assertNotNull(cacheEntry);
+
+ assertTrue(cacheEntry.getValue().getSecondName().contains("tName18"));
+ }
+
+ /** */
+ @Test
+ public void testFindOneValue() {
+ PersonProjection person = repo.findTopBySecondNameStartingWith("lastName18");
+
+ assertNotNull(person);
+
+ assertTrue(person.getFullName().split("\\s")[1].startsWith("lastName18"));
+ }
+
+ /** */
+ @Test
+ public void testSelectSeveralFields() {
+ List<List> lists = repo.selectSeveralField("^[a-z]+$", PageRequest.of(2, 6));
+
+ assertEquals(6, lists.size());
+
+ for (List list : lists) {
+ assertEquals(2, list.size());
+
+ assertTrue(list.get(0) instanceof Integer);
+ }
+ }
+
+ /** */
+ @Test
+ public void testCountQuery() {
+ int cnt = repo.countQuery(".*");
+
+ assertEquals(256, cnt);
+ }
+
+ /** */
+ @Test
+ public void testSliceOfCacheEntries() {
+ Slice<Cache.Entry<Integer, Person>> slice = repo2.findBySecondNameIsNot("lastName18", PageRequest.of(3, 4));
+
+ assertEquals(4, slice.getSize());
+
+ for (Cache.Entry<Integer, Person> entry : slice)
+ assertFalse("lastName18".equals(entry.getValue().getSecondName()));
+ }
+
+ /** */
+ @Test
+ public void testSliceOfLists() {
+ Slice<List> lists = repo2.querySliceOfList("^[a-z]+$", PageRequest.of(0, 3));
+
+ assertEquals(3, lists.getSize());
+
+ for (List list : lists) {
+ assertEquals(2, list.size());
+
+ assertTrue(list.get(0) instanceof Integer);
+ }
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
new file mode 100644
index 0000000..e86a575
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.Objects;
+
+/**
+ * Value-class
+ * */
+public class City {
+ /** City name */
+ private String name;
+
+ /** City district */
+ private String district;
+
+ /** City population */
+ private int population;
+
+ /**
+ * @param name city name
+ * @param district city district
+ * @param population city population
+ * */
+ public City(String name, String district, int population) {
+ this.name = name;
+ this.district = district;
+ this.population = population;
+ }
+
+ /**
+ * @return city name
+ * */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name city name
+ * */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return city district
+ * */
+ public String getDistrict() {
+ return district;
+ }
+
+ /**
+ * @param district city district
+ * */
+ public void setDistrict(String district) {
+ this.district = district;
+ }
+
+ /**
+ * @return city population
+ * */
+ public int getPopulation() {
+ return population;
+ }
+
+ /**
+ * @param population city population
+ * */
+ public void setPopulation(int population) {
+ this.population = population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return name + " | " + district + " | " + population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ City city = (City)o;
+
+ return
+ Objects.equals(this.name, city.name) &&
+ Objects.equals(this.district, city.district) &&
+ this.population == city.population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(name, district, population);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
new file mode 100644
index 0000000..88226fe
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
@@ -0,0 +1,79 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/** Compound key for city class */
+public class CityKey implements Serializable {
+ /** city identifier */
+ private int ID;
+
+ /** affinity key countrycode */
+ @AffinityKeyMapped
+ private String COUNTRYCODE;
+
+ /**
+ * @param id city identifier
+ * @param countryCode city countrycode
+ * */
+ public CityKey(int id, String countryCode) {
+ this.ID = id;
+ this.COUNTRYCODE = countryCode;
+ }
+
+ /**
+ * @return city id
+ * */
+ public int getId() {
+ return ID;
+ }
+
+ /**
+ * @return countrycode
+ * */
+ public String getCountryCode() {
+ return COUNTRYCODE;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ CityKey key = (CityKey)o;
+
+ return ID == key.ID &&
+ COUNTRYCODE.equals(key.COUNTRYCODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(ID, COUNTRYCODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return ID + " | " + COUNTRYCODE;
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
new file mode 100644
index 0000000..ab745e7
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.springdata.compoundkey;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.stereotype.Repository;
+
+/** City repository */
+@Repository
+@RepositoryConfig(cacheName = "City", autoCreateCache = true)
+public interface CityRepository extends IgniteRepository<City, CityKey> {
+ /**
+ * Find city by id
+ * @param id city identifier
+ * @return city
+ * */
+ public City findById(int id);
+
+ /**
+ * Find all cities by coutrycode
+ * @param cc coutrycode
+ * @return list of cache enrties CityKey -> City
+ * */
+ public List<Cache.Entry<CityKey, City>> findByCountryCode(String cc);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
new file mode 100644
index 0000000..2d472a2
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.springdata.compoundkey;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata22.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring application configuration
+ * */
+@Configuration
+@EnableIgniteRepositories
+public class CompoundKeyApplicationConfiguration {
+ /** */
+ public static final int CLI_CONN_PORT = 10810;
+
+ /**
+ * Ignite instance bean
+ * */
+ @Bean
+ public Ignite igniteInstance() {
+ return Ignition.start(new IgniteConfiguration()
+ .setClientConnectorConfiguration(new ClientConnectorConfiguration().setPort(CLI_CONN_PORT)));
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
new file mode 100644
index 0000000..1443d5d
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.springdata.misc.SampleEvaluationContextExtension.SamplePassParamExtension;
+import org.apache.ignite.springdata22.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.spel.spi.EvaluationContextExtension;
+
+/** */
+@Configuration
+@EnableIgniteRepositories
+public class ApplicationConfiguration {
+ /** */
+ public static final String IGNITE_INSTANCE_ONE = "IGNITE_INSTANCE_ONE";
+
+ /** */
+ public static final String IGNITE_INSTANCE_TWO = "IGNITE_INSTANCE_TWO";
+
+ /**
+ * The bean with cache names
+ */
+ @Bean
+ public CacheNamesBean cacheNames() {
+ CacheNamesBean bean = new CacheNamesBean();
+
+ bean.setPersonCacheName("PersonCache");
+
+ return bean;
+ }
+
+ /** */
+ @Bean
+ public EvaluationContextExtension sampleSpELExtension() {
+ return new SampleEvaluationContextExtension();
+ }
+
+ /** */
+ @Bean(value = "sampleExtensionBean")
+ public SamplePassParamExtension sampleExtensionBean() {
+ return new SamplePassParamExtension();
+ }
+
+ /**
+ * Ignite instance bean - no instance name provided on RepositoryConfig
+ */
+ @Bean
+ public Ignite igniteInstance() {
+ IgniteConfiguration cfg = new IgniteConfiguration();
+
+ cfg.setIgniteInstanceName(IGNITE_INSTANCE_ONE);
+
+ CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+ ccfg.setIndexedTypes(Integer.class, Person.class);
+
+ cfg.setCacheConfiguration(ccfg);
+
+ TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+ spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+ cfg.setDiscoverySpi(spi);
+
+ return Ignition.start(cfg);
+ }
+
+ /**
+ * Ignite instance bean with not default name
+ */
+ @Bean
+ public Ignite igniteInstanceTWO() {
+ IgniteConfiguration cfg = new IgniteConfiguration();
+
+ cfg.setIgniteInstanceName(IGNITE_INSTANCE_TWO);
+
+ CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+ ccfg.setIndexedTypes(Integer.class, Person.class);
+
+ cfg.setCacheConfiguration(ccfg);
+
+ TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+ spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+ cfg.setDiscoverySpi(spi);
+
+ return Ignition.start(cfg);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
new file mode 100644
index 0000000..6185744
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
@@ -0,0 +1,42 @@
+/*
+ * 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.springdata.misc;
+
+/**
+ * The bean with cache names.
+ */
+public class CacheNamesBean {
+ /** */
+ private String personCacheName;
+
+ /**
+ * Get name of cache for persons.
+ *
+ * @return Name of cache.
+ */
+ public String getPersonCacheName() {
+ return personCacheName;
+ }
+
+ /**
+ * @param personCacheName Name of cache.
+ */
+ public void setPersonCacheName(String personCacheName) {
+ this.personCacheName = personCacheName;
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java
new file mode 100644
index 0000000..23f8f8e
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java
@@ -0,0 +1,33 @@
+/*
+ * 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.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface FullNameProjection {
+ /**
+ * Sample of using SpEL expression
+ * @return
+ */
+ @Value("#{target.firstName + ' ' + target.secondName}")
+ String getFullName();
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
new file mode 100644
index 0000000..531c67f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
@@ -0,0 +1,100 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Objects;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * DTO class.
+ */
+public class Person {
+ /** First name. */
+ @QuerySqlField(index = true)
+ @QueryTextField
+ private String firstName;
+
+ /** Second name. */
+ @QuerySqlField(index = true)
+ private String secondName;
+
+ /**
+ * @param firstName First name.
+ * @param secondName Second name.
+ */
+ public Person(String firstName, String secondName) {
+ this.firstName = firstName;
+ this.secondName = secondName;
+ }
+
+ /**
+ * @return First name.
+ */
+ public String getFirstName() {
+ return firstName;
+ }
+
+ /**
+ * @param firstName First name.
+ */
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ /**
+ * @return Second name.
+ */
+ public String getSecondName() {
+ return secondName;
+ }
+
+ /**
+ * @param secondName Second name.
+ */
+ public void setSecondName(String secondName) {
+ this.secondName = secondName;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "Person{" +
+ "firstName='" + firstName + '\'' +
+ ", secondName='" + secondName + '\'' +
+ '}';
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ Person person = (Person)o;
+
+ return Objects.equals(firstName, person.firstName) &&
+ Objects.equals(secondName, person.secondName);
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(firstName, secondName);
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java
new file mode 100644
index 0000000..689f856
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java
@@ -0,0 +1,28 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+
+/**
+ *
+ */
+@RepositoryConfig(cacheName = "#{cacheNames.personCacheName}")
+public interface PersonExpressionRepository extends IgniteRepository<Person, Integer> {
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
new file mode 100644
index 0000000..2537c57
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
@@ -0,0 +1,54 @@
+/*
+ * 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.springdata.misc;
+
+import java.io.Serializable;
+
+/**
+ * Compound key.
+ */
+public class PersonKey implements Serializable {
+ /** */
+ private int id1;
+
+ /** */
+ private int id2;
+
+ /**
+ * @param id1 ID1.
+ * @param id2 ID2.
+ */
+ public PersonKey(int id1, int id2) {
+ this.id1 = id1;
+ this.id2 = id2;
+ }
+
+ /**
+ * @return ID1
+ */
+ public int getId1() {
+ return id1;
+ }
+
+ /**
+ * @return ID1
+ */
+ public int getId2() {
+ return id1;
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
new file mode 100644
index 0000000..a187a08
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface PersonProjection {
+ /** */
+ String getFirstName();
+
+ /**
+ * Sample of using registered spring bean into SpEL expression
+ * @return
+ */
+ @Value("#{@sampleExtensionBean.transformParam(target.firstName)}")
+ String getFirstNameTransformed();
+
+ /**
+ * Sample of using SpEL expression
+ * @return
+ */
+ @Value("#{target.firstName + ' ' + target.secondName}")
+ String getFullName();
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
new file mode 100644
index 0000000..0a5826f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -0,0 +1,151 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Integer> {
+ /** */
+ public List<Person> findByFirstName(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public <P> List<P> queryByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public <P> P queryOneByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+ /** */
+ @Query("firstName = ?#{[1]}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+ /** */
+ @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+ public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+ @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+ /** */
+ public List<Person> findByFirstNameContaining(String val);
+
+ /** */
+ public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+ /** */
+ public Collection<Person> findTopByFirstNameContaining(String val);
+
+ /** */
+ public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstName(String val);
+
+ /** */
+ public int countByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+ /** */
+ public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+ /** */
+ public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+ /** */
+ public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+ /** */
+ public PersonProjection findTopBySecondNameStartingWith(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<Person> simpleQuery(String val);
+
+ /** */
+ @Query("firstName REGEXP ?")
+ public List<Person> queryWithSort(String val, Sort sort);
+
+ /** */
+ @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+ public List<Person> queryWithPageable(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+ public List<String> selectField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public List<List> selectSeveralField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+ public int countQuery(String val);
+
+ /** Top 3 query */
+ public List<Person> findTop3ByFirstName(String val);
+
+ /** Delete query */
+ public long deleteByFirstName(String firstName);
+
+ /** Remove Query */
+ public List<Person> removeByFirstName(String firstName);
+
+ /** Delete using @Query with keyword in lower-case */
+ @Query("delete FROM Person WHERE secondName = ?")
+ public void deleteBySecondNameLowerCase(String secondName);
+
+ /** Delete using @Query but with errors on the query */
+ @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public void deleteWrongByFirstNameQuery(String firstName);
+
+ /** Update using @Query with keyword in mixed-case */
+ @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+ public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+ /** Update using @Query but with errors on the query */
+ @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
new file mode 100644
index 0000000..307556b
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
@@ -0,0 +1,143 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ *
+ */
+@RepositoryConfig(igniteInstance = "igniteInstanceTWO", cacheName = "PersonCache")
+public interface PersonRepositoryOtherIgniteInstance extends IgniteRepository<Person, Integer> {
+ /** */
+ public List<Person> findByFirstName(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+ /** */
+ @Query("firstName = :firstname")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ /** */
+ @Query("firstName = ?#{[1]}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+ /** */
+ @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+ public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+ @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+ @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+ public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+ /** */
+ public List<Person> findByFirstNameContaining(String val);
+
+ /** */
+ public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+ /** */
+ public Collection<Person> findTopByFirstNameContaining(String val);
+
+ /** */
+ public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstName(String val);
+
+ /** */
+ public int countByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+ /** */
+ public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+ /** */
+ public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+ /** */
+ public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+ /** */
+ public PersonProjection findTopBySecondNameStartingWith(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<Person> simpleQuery(String val);
+
+ /** */
+ @Query("firstName REGEXP ?")
+ public List<Person> queryWithSort(String val, Sort sort);
+
+ /** */
+ @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+ public List<Person> queryWithPageable(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+ public List<String> selectField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public List<List> selectSeveralField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+ public int countQuery(String val);
+
+ /** Top 3 query */
+ public List<Person> findTop3ByFirstName(String val);
+
+ /** Delete query */
+ public long deleteByFirstName(String firstName);
+
+ /** Remove Query */
+ public List<Person> removeByFirstName(String firstName);
+
+ /** Delete using @Query with keyword in lower-case */
+ @Query("delete FROM Person WHERE secondName = ?")
+ public void deleteBySecondNameLowerCase(String secondName);
+
+ /** Delete using @Query but with errors on the query */
+ @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public void deleteWrongByFirstNameQuery(String firstName);
+
+ /** Update using @Query with keyword in mixed-case */
+ @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+ public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+ /** Update using @Query but with errors on the query */
+ @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+ public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
new file mode 100644
index 0000000..97b0b5f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
@@ -0,0 +1,28 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonWithKeyCache", autoCreateCache = true)
+public interface PersonRepositoryWithCompoundKey extends IgniteRepository<Person, PersonKey> {
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
new file mode 100644
index 0000000..24f1c61
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
@@ -0,0 +1,41 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.data.domain.AbstractPageRequest;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonSecondRepository extends IgniteRepository<Person, Integer> {
+ /** */
+ public Slice<Cache.Entry<Integer, Person>> findBySecondNameIsNot(String val, PageRequest pageReq);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public Slice<List> querySliceOfList(String val, AbstractPageRequest pageReq);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
new file mode 100644
index 0000000..9db4343
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.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.springdata.misc;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.data.spel.spi.EvaluationContextExtension;
+
+/**
+ * Sample EvaluationContext Extension for Spring Data 2.2
+ * <p>
+ * Use SpEl expressions into your {@code @Query} definitions.
+ * <p>
+ * First, you need to register your extension into your spring data configuration. Sample:
+ * <pre>
+ * {@code @Configuration}
+ * {@code @EnableIgniteRepositories}(basePackages = ... )
+ * public class MyIgniteRepoConfig {
+ * ...
+ * {@code @Bean}
+ * public EvaluationContextExtension sampleSpELExtension() {
+ * return new SampleEvaluationContextExtension();
+ * }
+ * ...
+ * }
+ * </pre>
+ *
+ * <p>
+ * Sample of usage into your {@code @Query} definitions:
+ * <pre>
+ * {@code @RepositoryConfig}(cacheName = "users")
+ * public interface UserRepository
+ * extends IgniteRepository<User, UUID>{
+ * [...]
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = ?#{sampleExtension.transformParam(#email)}")
+ * User searchUserByEmail(@Param("email") String email);
+
+ * [...]
+ * }
+ * </pre>
+ * <p>
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class SampleEvaluationContextExtension implements EvaluationContextExtension {
+ /** */
+ private static final SamplePassParamExtension SAMPLE_PASS_PARAM_EXTENSION_INSTANCE = new SamplePassParamExtension();
+
+ /** */
+ private static final Map<String, Object> properties = new HashMap<>();
+
+ /** */
+ private static final String SAMPLE_EXTENSION_SPEL_VAR = "sampleExtension";
+
+ static {
+ properties.put(SAMPLE_EXTENSION_SPEL_VAR, SAMPLE_PASS_PARAM_EXTENSION_INSTANCE);
+ }
+
+ /** */
+ @Override public String getExtensionId() {
+ return "HK-SAMPLE-PASS-PARAM-EXTENSION";
+ }
+
+ /** */
+ @Override public Map<String, Object> getProperties() {
+ return properties;
+ }
+
+ /** */
+ public static class SamplePassParamExtension {
+ // just return same param
+ /** */
+ public Object transformParam(Object param) {
+ return param;
+ }
+ }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData22TestSuite.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData22TestSuite.java
new file mode 100644
index 0000000..872fe9f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData22TestSuite.java
@@ -0,0 +1,38 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.springdata.IgniteSpringDataCompoundKeyTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfExpressionTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfTest;
+import org.apache.ignite.springdata.IgniteSpringDataQueriesSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Spring Data 2.2 test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ IgniteSpringDataCrudSelfTest.class,
+ IgniteSpringDataQueriesSelfTest.class,
+ IgniteSpringDataCrudSelfExpressionTest.class,
+ IgniteSpringDataCompoundKeyTest.class
+})
+public class IgniteSpringData22TestSuite {
+}
diff --git a/modules/spring-data-ext/README.txt b/modules/spring-data-ext/README.txt
new file mode 100644
index 0000000..eb7aad9
--- /dev/null
+++ b/modules/spring-data-ext/README.txt
@@ -0,0 +1,28 @@
+Apache Ignite Spring Module
+---------------------------
+
+Apache Ignite Spring Data extension provides an integration with Spring Data framework.
+
+Importing Spring Data Module In Maven Project
+----------------------------------------
+
+If you are using Maven to manage dependencies of your project, you can add Spring Data extension
+dependency like this (replace '${ignite-spring-data-ext.version}' with actual version of Ignite Spring Data extension
+you are interested in):
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ ...
+ <dependencies>
+ ...
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring-data-ext</artifactId>
+ <version>${ignite-spring-data-ext.version}</version>
+ </dependency>
+ ...
+ </dependencies>
+ ...
+</project>
diff --git a/modules/spring-data-ext/licenses/apache-2.0.txt b/modules/spring-data-ext/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/spring-data-ext/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/modules/spring-data-ext/modules/core/src/test/config/log4j-test.xml b/modules/spring-data-ext/modules/core/src/test/config/log4j-test.xml
new file mode 100755
index 0000000..3061bd4
--- /dev/null
+++ b/modules/spring-data-ext/modules/core/src/test/config/log4j-test.xml
@@ -0,0 +1,97 @@
+<?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.
+-->
+
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+ "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+<!--
+ Log4j configuration.
+-->
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+ <!--
+ Logs System.out messages to console.
+ -->
+ <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+ <!-- Log to STDOUT. -->
+ <param name="Target" value="System.out"/>
+
+ <!-- Log from DEBUG and higher. -->
+ <param name="Threshold" value="DEBUG"/>
+
+ <!-- The default pattern: Date Priority [Category] Message\n -->
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+
+ <!-- Do not log beyond INFO level. -->
+ <filter class="org.apache.log4j.varia.LevelRangeFilter">
+ <param name="levelMin" value="DEBUG"/>
+ <param name="levelMax" value="INFO"/>
+ </filter>
+ </appender>
+
+ <!--
+ Logs all System.err messages to console.
+ -->
+ <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+ <!-- Log to STDERR. -->
+ <param name="Target" value="System.err"/>
+
+ <!-- Log from WARN and higher. -->
+ <param name="Threshold" value="WARN"/>
+
+ <!-- The default pattern: Date Priority [Category] Message\n -->
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+ </appender>
+
+ <!--
+ Logs all output to specified file.
+ -->
+ <appender name="FILE" class="org.apache.log4j.RollingFileAppender">
+ <param name="Threshold" value="DEBUG"/>
+ <param name="File" value="ignite/work/log/ignite.log"/>
+ <param name="Append" value="true"/>
+ <param name="MaxFileSize" value="10MB"/>
+ <param name="MaxBackupIndex" value="10"/>
+ <layout class="org.apache.log4j.PatternLayout">
+ <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+ </layout>
+ </appender>
+
+ <!-- Disable all open source debugging. -->
+ <category name="org">
+ <level value="INFO"/>
+ </category>
+
+ <category name="org.eclipse.jetty">
+ <level value="INFO"/>
+ </category>
+
+ <!-- Default settings. -->
+ <root>
+ <!-- Print at info by default. -->
+ <level value="INFO"/>
+
+ <!-- Append to file and console. -->
+ <appender-ref ref="FILE"/>
+ <appender-ref ref="CONSOLE"/>
+ <appender-ref ref="CONSOLE_ERR"/>
+ </root>
+</log4j:configuration>
diff --git a/modules/spring-data-ext/modules/core/src/test/config/tests.properties b/modules/spring-data-ext/modules/core/src/test/config/tests.properties
new file mode 100644
index 0000000..0faf5b8
--- /dev/null
+++ b/modules/spring-data-ext/modules/core/src/test/config/tests.properties
@@ -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.
+#
+
+# Local address to bind to.
+local.ip=127.0.0.1
+
+# TCP communication port
+comm.tcp.port=30010
diff --git a/modules/spring-data-ext/pom.xml b/modules/spring-data-ext/pom.xml
new file mode 100644
index 0000000..b7547f1
--- /dev/null
+++ b/modules/spring-data-ext/pom.xml
@@ -0,0 +1,100 @@
+<?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.
+-->
+
+<!--
+ POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-extensions-parent</artifactId>
+ <version>1</version>
+ <relativePath>../../parent</relativePath>
+ </parent>
+
+ <artifactId>ignite-spring-data-ext</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <url>http://ignite.apache.org</url>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-indexing</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-log4j</artifactId>
+ <version>${ignite.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.data</groupId>
+ <artifactId>spring-data-commons</artifactId>
+ <version>${spring.data.version}</version>
+ <!-- Exclude slf4j logging in favor of log4j -->
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-beans</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-spring</artifactId>
+ <version>${ignite.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-tools</artifactId>
+ <version>${ignite.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/IgniteRepository.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/IgniteRepository.java
new file mode 100644
index 0000000..472d2e0
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/IgniteRepository.java
@@ -0,0 +1,58 @@
+/*
+ * 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.springdata.repository;
+
+import java.io.Serializable;
+import java.util.Map;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Apache Ignite repository that extends basic capabilities of {@link CrudRepository}.
+ */
+public interface IgniteRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
+ /**
+ * Saves a given entity using provided key.
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates
+ * IDs (keys) that are not unique cluster wide.
+ *
+ * @param key Entity's key.
+ * @param entity Entity to save.
+ * @param <S> Entity type.
+ * @return Saved entity.
+ */
+ <S extends T> S save(ID key, S entity);
+
+ /**
+ * Saves all given keys and entities combinations.
+ * </p>
+ * It's suggested to use this method instead of default {@link CrudRepository#save(Iterable)} that generates
+ * IDs (keys) that are not unique cluster wide.
+ *
+ * @param entities Map of key-entities pairs to save.
+ * @param <S> type of entities.
+ * @return Saved entities.
+ */
+ <S extends T> Iterable<S> save(Map<ID, S> entities);
+
+ /**
+ * Deletes all the entities for the provided ids.
+ *
+ * @param ids List of ids to delete.
+ */
+ void deleteAll(Iterable<ID> ids);
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/EnableIgniteRepositories.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/EnableIgniteRepositories.java
new file mode 100644
index 0000000..f73fc3d
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/EnableIgniteRepositories.java
@@ -0,0 +1,119 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.springdata.repository.support.IgniteRepositoryFactoryBean;
+import org.apache.ignite.springdata.repository.support.IgniteRepositoryImpl;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate Apache Ignite repositories. If no base package is configured through either {@link #value()},
+ * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(IgniteRepositoriesRegistar.class)
+public @interface EnableIgniteRepositories {
+ /**
+ * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
+ * {@code @EnableIgniteRepositories("org.my.pkg")} instead of
+ * {@code @EnableIgniteRepositories(basePackages="org.my.pkg")}.
+ */
+ String[] value() default {};
+
+ /**
+ * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with)
+ * this attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+ */
+ String[] basePackages() default {};
+
+ /**
+ * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components.
+ * The package of each class specified will be scanned. Consider creating a special no-op marker class or interface
+ * in each package that serves no purpose other than being referenced by this attribute.
+ */
+ Class<?>[] basePackageClasses() default {};
+
+ /**
+ * Specifies which types are not eligible for component scanning.
+ */
+ Filter[] excludeFilters() default {};
+
+ /**
+ * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+ * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or
+ * filters.
+ */
+ Filter[] includeFilters() default {};
+
+ /**
+ * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+ * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+ * for {@code PersonRepositoryImpl}.
+ *
+ * @return Postfix to be used when looking up custom repository implementations.
+ */
+ String repositoryImplementationPostfix() default "Impl";
+
+ /**
+ * Configures the location of where to find the Spring Data named queries properties file.
+ *
+ * @return Location of where to find the Spring Data named queries properties file.
+ */
+ String namedQueriesLocation() default "";
+
+ /**
+ * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+ * {@link Key#CREATE_IF_NOT_FOUND}.
+ *
+ * @return Key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods.
+ */
+ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+ /**
+ * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
+ * {@link IgniteRepositoryFactoryBean}.
+ *
+ * @return {@link FactoryBean} class to be used for each repository instance.
+ */
+ Class<?> repositoryFactoryBeanClass() default IgniteRepositoryFactoryBean.class;
+
+ /**
+ * Configure the repository base class to be used to create repository proxies for this particular configuration.
+ *
+ * @return Repository base class to be used to create repository proxies for this particular configuration.
+ */
+ Class<?> repositoryBaseClass() default IgniteRepositoryImpl.class;
+
+ /**
+ * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+ * repositories infrastructure.
+ */
+ boolean considerNestedRepositories() default false;
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoriesRegistar.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoriesRegistar.java
new file mode 100644
index 0000000..0f65c32
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoriesRegistar.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.springdata.repository.config;
+
+import java.lang.annotation.Annotation;
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryBeanDefinitionRegistrarSupport}.
+ */
+public class IgniteRepositoriesRegistar extends RepositoryBeanDefinitionRegistrarSupport {
+ /** {@inheritDoc} */
+ @Override protected Class<? extends Annotation> getAnnotation() {
+ return EnableIgniteRepositories.class;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryConfigurationExtension getExtension() {
+ return new IgniteRepositoryConfigurationExtension();
+ }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoryConfigurationExtension.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoryConfigurationExtension.java
new file mode 100644
index 0000000..630690a
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoryConfigurationExtension.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryConfigurationExtension}.
+ */
+public class IgniteRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
+ /** {@inheritDoc} */
+ @Override public String getModuleName() {
+ return "Apache Ignite";
+ }
+
+ /** {@inheritDoc} */
+ @Override protected String getModulePrefix() {
+ return "ignite";
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getRepositoryFactoryClassName() {
+ return IgniteRepositoryFactoryBean.class.getName();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Collection<Class<?>> getIdentifyingTypes() {
+ return Collections.<Class<?>>singleton(IgniteRepository.class);
+ }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/Query.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/Query.java
new file mode 100644
index 0000000..1095942
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/Query.java
@@ -0,0 +1,37 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to provide a user defined SQL query for a method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Query {
+ /**
+ * SQL query text string.
+ */
+ String value() default "";
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/RepositoryConfig.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/RepositoryConfig.java
new file mode 100644
index 0000000..4a3703e
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/RepositoryConfig.java
@@ -0,0 +1,39 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The annotation can be used to pass Ignite specific parameters to a bound repository.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepositoryConfig {
+ /**
+ * @return A name of a distributed Apache Ignite cache an annotated repository will be mapped to.
+ */
+ String cacheName() default "";
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/package-info.java
new file mode 100644
index 0000000..c7f473b
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/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. -->
+ * Package includes Spring Data integration related configuration files.
+ */
+
+package org.apache.ignite.springdata.repository.config;
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/package-info.java
new file mode 100644
index 0000000..773abea
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/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. -->
+ * Package contains Apache Ignite Spring Data integration.
+ */
+
+package org.apache.ignite.springdata.repository;
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
new file mode 100644
index 0000000..f630ca0
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
@@ -0,0 +1,83 @@
+/*
+ * 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.springdata.repository.query;
+
+/**
+ * Ignite query helper class. For internal use only.
+ */
+public class IgniteQuery {
+ /** */
+ enum Option {
+ /** Query will be used with Sort object. */
+ SORTING,
+
+ /** Query will be used with Pageable object. */
+ PAGINATION,
+
+ /** No advanced option. */
+ NONE
+ }
+
+ /** Sql query text string. */
+ private final String sql;
+
+ /** */
+ private final boolean isFieldQuery;
+
+ /** Type of option. */
+ private final Option option;
+
+ /**
+ * @param sql Sql.
+ * @param isFieldQuery Is field query.
+ * @param option Option.
+ */
+ public IgniteQuery(String sql, boolean isFieldQuery, Option option) {
+ this.sql = sql;
+ this.isFieldQuery = isFieldQuery;
+ this.option = option;
+ }
+
+ /**
+ * Text string of the query.
+ *
+ * @return SQL query text string.
+ */
+ public String sql() {
+ return sql;
+ }
+
+ /**
+ * Returns {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+ *
+ * @return {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+ */
+ public boolean isFieldQuery() {
+ return isFieldQuery;
+ }
+
+ /**
+ * Advanced querying option.
+ *
+ * @return querying option.
+ */
+ public Option options() {
+ return option;
+ }
+}
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
new file mode 100644
index 0000000..e1f3f2f
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
@@ -0,0 +1,265 @@
+/*
+ * 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.springdata.repository.query;
+
+import java.lang.reflect.Method;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * Ignite query generator for Spring Data framework.
+ */
+public class IgniteQueryGenerator {
+
+ /**
+ * @param mtd Method.
+ * @param metadata Metadata.
+ */
+ @NotNull public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
+ PartTree parts;
+
+ try {
+ parts = new PartTree(mtd.getName(), metadata.getDomainType());
+ }
+ catch (PropertyReferenceException e) {
+ parts = new PartTree(mtd.getName(), metadata.getIdType());
+ }
+
+ StringBuilder sql = new StringBuilder();
+
+ if (parts.isDelete())
+ throw new UnsupportedOperationException("DELETE clause is not supported now.");
+ else {
+ sql.append("SELECT ");
+
+ if (parts.isDistinct())
+ throw new UnsupportedOperationException("DISTINCT clause in not supported.");
+
+ if (parts.isCountProjection())
+ sql.append("COUNT(1) ");
+ else
+ sql.append(" * ");
+ }
+
+ sql.append("FROM ").append(metadata.getDomainType().getSimpleName());
+
+ if (parts.iterator().hasNext()) {
+ sql.append(" WHERE ");
+
+ for (PartTree.OrPart orPart : parts) {
+ sql.append("(");
+ for (Part part : orPart) {
+ handleQueryPart(sql, part, metadata.getDomainType());
+ sql.append(" AND ");
+ }
+
+ sql.delete(sql.length() - 5, sql.length());
+
+ sql.append(") OR ");
+ }
+
+ sql.delete(sql.length() - 4, sql.length());
+ }
+
+ addSorting(sql, parts.getSort());
+
+ if (parts.isLimiting()) {
+ sql.append(" LIMIT ");
+ sql.append(parts.getMaxResults().intValue());
+ }
+
+ return new IgniteQuery(sql.toString(), parts.isCountProjection(), getOptions(mtd));
+ }
+
+ /**
+ * Add a dynamic part of query for the sorting support.
+ *
+ * @param sql SQL text string.
+ * @param sort Sort method.
+ */
+ public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
+ if (sort != null) {
+ sql.append(" ORDER BY ");
+
+ for (Sort.Order order : sort) {
+ sql.append(order.getProperty()).append(" ").append(order.getDirection());
+
+ if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
+ sql.append(" ").append("NULL ");
+ switch (order.getNullHandling()) {
+ case NULLS_FIRST:
+ sql.append("FIRST");
+ break;
+ case NULLS_LAST:
+ sql.append("LAST");
+ break;
+ }
+ }
+ sql.append(", ");
+ }
+
+ sql.delete(sql.length() - 2, sql.length());
+ }
+
+ return sql;
+ }
+
+ /**
+ * Add a dynamic part of a query for the pagination support.
+ *
+ * @param sql Builder instance.
+ * @param pageable Pageable instance.
+ * @return Builder instance.
+ */
+ public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {
+ if (pageable.getSort() != null)
+ addSorting(sql, pageable.getSort());
+
+ sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());
+
+ return sql;
+ }
+
+ /**
+ * Determines whether query is dynamic or not (by list of method parameters)
+ *
+ * @param mtd Method.
+ * @return type of options
+ */
+ public static IgniteQuery.Option getOptions(Method mtd) {
+ IgniteQuery.Option option = IgniteQuery.Option.NONE;
+
+ Class<?>[] types = mtd.getParameterTypes();
+
+ if (types.length > 0) {
+ Class<?> type = types[types.length - 1];
+
+ if (Sort.class.isAssignableFrom(type))
+ option = IgniteQuery.Option.SORTING;
+ else if (Pageable.class.isAssignableFrom(type))
+ option = IgniteQuery.Option.PAGINATION;
+ }
+
+ for (int i = 0; i < types.length - 1; i++) {
+ Class<?> tp = types[i];
+ if (tp == Sort.class || tp == Pageable.class)
+ throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
+ }
+
+ return option;
+ }
+
+ /**
+ * Check and correct table name if using column name from compound key.
+ */
+ private static String getColumnName(Part part, Class<?> domainType) {
+ PropertyPath prperty = part.getProperty();
+
+ if (prperty.getType() != domainType)
+ return domainType.getSimpleName() + "." + prperty.getSegment();
+ else
+ return part.toString();
+ }
+
+ /**
+ * Transform part to sql expression
+ */
+ private static void handleQueryPart(StringBuilder sql, Part part, Class<?> domainType) {
+ sql.append("(");
+
+ sql.append(getColumnName(part, domainType));
+
+ switch (part.getType()) {
+ case SIMPLE_PROPERTY:
+ sql.append("=?");
+ break;
+ case NEGATING_SIMPLE_PROPERTY:
+ sql.append("<>?");
+ break;
+ case GREATER_THAN:
+ sql.append(">?");
+ break;
+ case GREATER_THAN_EQUAL:
+ sql.append(">=?");
+ break;
+ case LESS_THAN:
+ sql.append("<?");
+ break;
+ case LESS_THAN_EQUAL:
+ sql.append("<=?");
+ break;
+ case IS_NOT_NULL:
+ sql.append(" IS NOT NULL");
+ break;
+ case IS_NULL:
+ sql.append(" IS NULL");
+ break;
+ case BETWEEN:
+ sql.append(" BETWEEN ? AND ?");
+ break;
+ case FALSE:
+ sql.append(" = FALSE");
+ break;
+ case TRUE:
+ sql.append(" = TRUE");
+ break;
+ case CONTAINING:
+ sql.append(" LIKE '%' || ? || '%'");
+ break;
+ case NOT_CONTAINING:
+ sql.append(" NOT LIKE '%' || ? || '%'");
+ break;
+ case LIKE:
+ sql.append(" LIKE '%' || ? || '%'");
+ break;
+ case NOT_LIKE:
+ sql.append(" NOT LIKE '%' || ? || '%'");
+ break;
+ case STARTING_WITH:
+ sql.append(" LIKE ? || '%'");
+ break;
+ case ENDING_WITH:
+ sql.append(" LIKE '%' || ?");
+ break;
+ case IN:
+ sql.append(" IN ?");
+ break;
+ case NOT_IN:
+ sql.append(" NOT IN ?");
+ break;
+ case REGEX:
+ sql.append(" REGEXP ?");
+ break;
+ case NEAR:
+ case AFTER:
+ case BEFORE:
+ case EXISTS:
+ default:
+ throw new UnsupportedOperationException(part.getType() + " is not supported!");
+ }
+
+ sql.append(")");
+ }
+}
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
new file mode 100644
index 0000000..e351c61
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
@@ -0,0 +1,326 @@
+/*
+ * 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.springdata.repository.query;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.internal.processors.cache.CacheEntryImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.RepositoryQuery;
+
+import static org.apache.ignite.springdata.repository.query.IgniteQueryGenerator.addPaging;
+import static org.apache.ignite.springdata.repository.query.IgniteQueryGenerator.addSorting;
+
+/**
+ * Ignite SQL query implementation.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class IgniteRepositoryQuery implements RepositoryQuery {
+ /** Defines the way how to process query result */
+ private enum ReturnStrategy {
+ /** Need to return only one value. */
+ ONE_VALUE,
+
+ /** Need to return one cache entry */
+ CACHE_ENTRY,
+
+ /** Need to return list of cache entries */
+ LIST_OF_CACHE_ENTRIES,
+
+ /** Need to return list of values */
+ LIST_OF_VALUES,
+
+ /** Need to return list of lists */
+ LIST_OF_LISTS,
+
+ /** Need to return slice */
+ SLICE_OF_VALUES,
+
+ /** Slice of cache entries. */
+ SLICE_OF_CACHE_ENTRIES,
+
+ /** Slice of lists. */
+ SLICE_OF_LISTS
+ }
+
+ /** Type. */
+ private final Class<?> type;
+
+ /** Sql. */
+ private final IgniteQuery qry;
+
+ /** Cache. */
+ private final IgniteCache cache;
+
+ /** Method. */
+ private final Method mtd;
+
+ /** Metadata. */
+ private final RepositoryMetadata metadata;
+
+ /** Factory. */
+ private final ProjectionFactory factory;
+
+ /** Return strategy. */
+ private final ReturnStrategy returnStgy;
+
+ /**
+ * @param metadata Metadata.
+ * @param qry Query.
+ * @param mtd Method.
+ * @param factory Factory.
+ * @param cache Cache.
+ */
+ public IgniteRepositoryQuery(RepositoryMetadata metadata, IgniteQuery qry,
+ Method mtd, ProjectionFactory factory, IgniteCache cache) {
+ type = metadata.getDomainType();
+ this.qry = qry;
+ this.cache = cache;
+ this.metadata = metadata;
+ this.mtd = mtd;
+ this.factory = factory;
+
+ returnStgy = calcReturnType(mtd, qry.isFieldQuery());
+ }
+
+ /** {@inheritDoc} */
+ @Override public Object execute(Object[] prmtrs) {
+ Query qry = prepareQuery(prmtrs);
+
+ try (QueryCursor qryCursor = cache.query(qry)) {
+ return transformQueryCursor(prmtrs, qryCursor);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override public QueryMethod getQueryMethod() {
+ return new QueryMethod(mtd, metadata, factory);
+ }
+
+ /**
+ * @param mtd Method.
+ * @param isFieldQry Is field query.
+ * @return Return strategy type.
+ */
+ private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) {
+ Class<?> returnType = mtd.getReturnType();
+
+ if (returnType.isAssignableFrom(ArrayList.class)) {
+ if (isFieldQry) {
+ if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+ return ReturnStrategy.LIST_OF_LISTS;
+ }
+ else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+ return ReturnStrategy.LIST_OF_CACHE_ENTRIES;
+
+ return ReturnStrategy.LIST_OF_VALUES;
+ }
+ else if (returnType == Slice.class) {
+ if (isFieldQry) {
+ if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+ return ReturnStrategy.SLICE_OF_LISTS;
+ }
+ else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+ return ReturnStrategy.SLICE_OF_CACHE_ENTRIES;
+
+ return ReturnStrategy.SLICE_OF_VALUES;
+ }
+ else if (Cache.Entry.class.isAssignableFrom(returnType))
+ return ReturnStrategy.CACHE_ENTRY;
+ else
+ return ReturnStrategy.ONE_VALUE;
+ }
+
+ /**
+ * @param cls Class 1.
+ * @param mtd Method.
+ * @return if {@code mtd} return type is assignable from {@code cls}
+ */
+ private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
+ Type[] actualTypeArguments = ((ParameterizedType)mtd.getGenericReturnType()).getActualTypeArguments();
+
+ if (actualTypeArguments.length == 0)
+ return false;
+
+ if (actualTypeArguments[0] instanceof ParameterizedType) {
+ ParameterizedType type = (ParameterizedType)actualTypeArguments[0];
+
+ Class<?> type1 = (Class)type.getRawType();
+
+ return type1.isAssignableFrom(cls);
+ }
+
+ if (actualTypeArguments[0] instanceof Class) {
+ Class typeArg = (Class)actualTypeArguments[0];
+
+ return typeArg.isAssignableFrom(cls);
+ }
+
+ return false;
+ }
+
+ /**
+ * @param prmtrs Prmtrs.
+ * @param qryCursor Query cursor.
+ * @return Query cursor or slice
+ */
+ @Nullable private Object transformQueryCursor(Object[] prmtrs, QueryCursor qryCursor) {
+ if (qry.isFieldQuery()) {
+ Iterable<List> qryIter = (Iterable<List>)qryCursor;
+
+ switch (returnStgy) {
+ case LIST_OF_VALUES:
+ List list = new ArrayList<>();
+
+ for (List entry : qryIter)
+ list.add(entry.get(0));
+
+ return list;
+
+ case ONE_VALUE:
+ Iterator<List> iter = qryIter.iterator();
+
+ if (iter.hasNext())
+ return iter.next().get(0);
+
+ return null;
+
+ case SLICE_OF_VALUES:
+ List content = new ArrayList<>();
+
+ for (List entry : qryIter)
+ content.add(entry.get(0));
+
+ return new SliceImpl(content, (Pageable)prmtrs[prmtrs.length - 1], true);
+
+ case SLICE_OF_LISTS:
+ return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+
+ case LIST_OF_LISTS:
+ return qryCursor.getAll();
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ else {
+ Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor;
+
+ switch (returnStgy) {
+ case LIST_OF_VALUES:
+ List list = new ArrayList<>();
+
+ for (CacheEntryImpl entry : qryIter)
+ list.add(entry.getValue());
+
+ return list;
+
+ case ONE_VALUE:
+ Iterator<CacheEntryImpl> iter1 = qryIter.iterator();
+
+ if (iter1.hasNext())
+ return iter1.next().getValue();
+
+ return null;
+
+ case CACHE_ENTRY:
+ Iterator<CacheEntryImpl> iter2 = qryIter.iterator();
+
+ if (iter2.hasNext())
+ return iter2.next();
+
+ return null;
+
+ case SLICE_OF_VALUES:
+ List content = new ArrayList<>();
+
+ for (CacheEntryImpl entry : qryIter)
+ content.add(entry.getValue());
+
+ return new SliceImpl(content, (Pageable)prmtrs[prmtrs.length - 1], true);
+
+ case SLICE_OF_CACHE_ENTRIES:
+ return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+
+ case LIST_OF_CACHE_ENTRIES:
+ return qryCursor.getAll();
+
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ /**
+ * @param prmtrs Prmtrs.
+ * @return prepared query for execution
+ */
+ @SuppressWarnings("deprecation")
+ @NotNull private Query prepareQuery(Object[] prmtrs) {
+ Object[] parameters = prmtrs;
+ String sql = qry.sql();
+
+ switch (qry.options()) {
+ case SORTING:
+ sql = addSorting(new StringBuilder(sql), (Sort)parameters[parameters.length - 1]).toString();
+ parameters = Arrays.copyOfRange(parameters, 0, parameters.length - 1);
+
+ break;
+
+ case PAGINATION:
+ sql = addPaging(new StringBuilder(sql), (Pageable)parameters[parameters.length - 1]).toString();
+ parameters = Arrays.copyOfRange(parameters, 0, parameters.length - 1);
+
+ break;
+
+ case NONE:
+ // No-op.
+ }
+
+ if (qry.isFieldQuery()) {
+ SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(sql);
+ sqlFieldsQry.setArgs(parameters);
+
+ return sqlFieldsQry;
+ }
+
+ SqlQuery sqlQry = new SqlQuery(type, sql);
+ sqlQry.setArgs(parameters);
+
+ return sqlQry;
+ }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/package-info.java
new file mode 100644
index 0000000..9b6fdb1
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/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. -->
+ * Package includes classes that integrates with Apache Ignite SQL engine.
+ */
+
+package org.apache.ignite.springdata.repository.query;
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
new file mode 100644
index 0000000..de2549f
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
@@ -0,0 +1,168 @@
+/*
+ * 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.springdata.repository.support;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.Query;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.apache.ignite.springdata.repository.query.IgniteQuery;
+import org.apache.ignite.springdata.repository.query.IgniteQueryGenerator;
+import org.apache.ignite.springdata.repository.query.IgniteRepositoryQuery;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.EntityInformation;
+import org.springframework.data.repository.core.NamedQueries;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractEntityInformation;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Crucial for spring-data functionality class. Create proxies for repositories.
+ */
+public class IgniteRepositoryFactory extends RepositoryFactorySupport {
+ /** Ignite instance */
+ private Ignite ignite;
+
+ /** Mapping of a repository to a cache. */
+ private final Map<Class<?>, String> repoToCache = new HashMap<>();
+
+ /**
+ * Creates the factory with initialized {@link Ignite} instance.
+ *
+ * @param ignite
+ */
+ public IgniteRepositoryFactory(Ignite ignite) {
+ this.ignite = ignite;
+ }
+
+ /**
+ * Initializes the factory with provided {@link IgniteConfiguration} that is used to start up an underlying
+ * {@link Ignite} instance.
+ *
+ * @param cfg Ignite configuration.
+ */
+ public IgniteRepositoryFactory(IgniteConfiguration cfg) {
+ this.ignite = Ignition.start(cfg);
+ }
+
+ /**
+ * Initializes the factory with provided a configuration under {@code springCfgPath} that is used to start up
+ * an underlying {@link Ignite} instance.
+ *
+ * @param springCfgPath A path to Ignite configuration.
+ */
+ public IgniteRepositoryFactory(String springCfgPath) {
+ this.ignite = Ignition.start(springCfgPath);
+ }
+
+ /** {@inheritDoc} */
+ @Override public <T, ID extends Serializable> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
+ return new AbstractEntityInformation<T, ID>(domainClass) {
+ @Override public ID getId(T entity) {
+ return null;
+ }
+
+ @Override public Class<ID> getIdType() {
+ return null;
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
+ return IgniteRepositoryImpl.class;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryMetadata getRepositoryMetadata(Class<?> repoItf) {
+ Assert.notNull(repoItf, "Repository interface must be set.");
+ Assert.isAssignable(IgniteRepository.class, repoItf, "Repository must implement IgniteRepository interface.");
+
+ RepositoryConfig annotation = repoItf.getAnnotation(RepositoryConfig.class);
+
+ Assert.notNull(annotation, "Set a name of an Apache Ignite cache using @RepositoryConfig annotation to map " +
+ "this repository to the underlying cache.");
+
+ Assert.hasText(annotation.cacheName(), "Set a name of an Apache Ignite cache using @RepositoryConfig " +
+ "annotation to map this repository to the underlying cache.");
+
+ repoToCache.put(repoItf, annotation.cacheName());
+
+ return super.getRepositoryMetadata(repoItf);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected Object getTargetRepository(RepositoryInformation metadata) {
+ return getTargetRepositoryViaReflection(metadata,
+ ignite.getOrCreateCache(repoToCache.get(metadata.getRepositoryInterface())));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected QueryLookupStrategy getQueryLookupStrategy(final QueryLookupStrategy.Key key,
+ EvaluationContextProvider evaluationCtxProvider) {
+
+ return new QueryLookupStrategy() {
+ @Override public RepositoryQuery resolveQuery(final Method mtd, final RepositoryMetadata metadata,
+ final ProjectionFactory factory, NamedQueries namedQueries) {
+
+ final Query annotation = mtd.getAnnotation(Query.class);
+
+ if (annotation != null) {
+ String qryStr = annotation.value();
+
+ if (key != Key.CREATE && StringUtils.hasText(qryStr))
+ return new IgniteRepositoryQuery(metadata,
+ new IgniteQuery(qryStr, isFieldQuery(qryStr), IgniteQueryGenerator.getOptions(mtd)),
+ mtd, factory, ignite.getOrCreateCache(repoToCache.get(metadata.getRepositoryInterface())));
+ }
+
+ if (key == QueryLookupStrategy.Key.USE_DECLARED_QUERY)
+ throw new IllegalStateException("To use QueryLookupStrategy.Key.USE_DECLARED_QUERY, pass " +
+ "a query string via org.apache.ignite.springdata.repository.config.Query annotation.");
+
+ return new IgniteRepositoryQuery(metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd,
+ factory, ignite.getOrCreateCache(repoToCache.get(metadata.getRepositoryInterface())));
+ }
+ };
+ }
+
+ /**
+ * @param qry Query string.
+ * @return {@code true} if query is SQLFieldsQuery.
+ */
+ private boolean isFieldQuery(String qry) {
+ return qry.matches("^SELECT.*") && !qry.matches("^SELECT\\s+(?:\\w+\\.)?+\\*.*");
+ }
+}
+
+
+
+
+
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactoryBean.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactoryBean.java
new file mode 100644
index 0000000..f87dce5
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactoryBean.java
@@ -0,0 +1,92 @@
+/*
+ * 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.springdata.repository.support;
+
+import java.io.Serializable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Apache Ignite repository factory bean.
+ *
+ * The repository requires to define one of the parameters below in your Spring application configuration in order
+ * to get an access to Apache Ignite cluster:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance"</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg"</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath"</li>
+ * <ul/>
+ *
+ * @param <T> Repository type, {@link IgniteRepository}
+ * @param <S> Domain object class.
+ * @param <ID> Domain object key, super expects {@link Serializable}.
+ */
+public class IgniteRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
+ extends RepositoryFactoryBeanSupport<T, S, ID> implements ApplicationContextAware {
+ /** Application context. */
+ private ApplicationContext ctx;
+
+ /**
+ * @param repositoryInterface Repository interface.
+ */
+ protected IgniteRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
+ super(repositoryInterface);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void setApplicationContext(ApplicationContext context) throws BeansException {
+ this.ctx = context;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RepositoryFactorySupport createRepositoryFactory() {
+ try {
+ Ignite ignite = (Ignite)ctx.getBean("igniteInstance");
+
+ return new IgniteRepositoryFactory(ignite);
+ }
+ catch (BeansException ex) {
+ try {
+ IgniteConfiguration cfg = (IgniteConfiguration)ctx.getBean("igniteCfg");
+
+ return new IgniteRepositoryFactory(cfg);
+ }
+ catch (BeansException ex2) {
+ try {
+ String path = (String)ctx.getBean("igniteSpringCfgPath");
+
+ return new IgniteRepositoryFactory(path);
+ }
+ catch (BeansException ex3) {
+ throw new IgniteException("Failed to initialize Ignite repository factory. Ignite instance or" +
+ " IgniteConfiguration or a path to Ignite's spring XML configuration must be defined in the" +
+ " application configuration");
+ }
+ }
+ }
+ }
+}
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryImpl.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryImpl.java
new file mode 100644
index 0000000..95d933b
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryImpl.java
@@ -0,0 +1,167 @@
+/*
+ * 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.springdata.repository.support;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.cache.Cache;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+
+import static java.util.Collections.emptySet;
+
+/**
+ * General Apache Ignite repository implementation.
+ */
+public class IgniteRepositoryImpl<T, ID extends Serializable> implements IgniteRepository<T, ID> {
+ /** Ignite Cache bound to the repository */
+ private final IgniteCache<ID, T> cache;
+
+ /**
+ * Repository constructor.
+ *
+ * @param cache Initialized cache instance.
+ */
+ public IgniteRepositoryImpl(IgniteCache<ID, T> cache) {
+ this.cache = cache;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends T> S save(ID key, S entity) {
+ cache.put(key, entity);
+
+ return entity;
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends T> Iterable<S> save(Map<ID, S> entities) {
+ cache.putAll(entities);
+
+ return entities.values();
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends T> S save(S entity) {
+ throw new UnsupportedOperationException("Use IgniteRepository.save(key,value) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public <S extends T> Iterable<S> save(Iterable<S> entities) {
+ throw new UnsupportedOperationException("Use IgniteRepository.save(Map<keys,value>) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public T findOne(ID id) {
+ return cache.get(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean exists(ID id) {
+ return cache.containsKey(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterable<T> findAll() {
+ final Iterator<Cache.Entry<ID, T>> iter = cache.iterator();
+
+ return new Iterable<T>() {
+ @Override public Iterator<T> iterator() {
+ return new Iterator<T>() {
+ @Override public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ @Override public T next() {
+ return iter.next().getValue();
+ }
+
+ @Override public void remove() {
+ iter.remove();
+ }
+ };
+ }
+ };
+ }
+
+ /**
+ * @param ids Collection of IDs.
+ * @return Collection transformed to set.
+ */
+ private Set<ID> toSet(Iterable<ID> ids) {
+ if (ids instanceof Set)
+ return (Set<ID>)ids;
+
+ Iterator<ID> itr = ids.iterator();
+
+ if (!itr.hasNext())
+ return emptySet();
+
+ ID key = itr.next();
+
+ Set<ID> keys = key instanceof Comparable ? new TreeSet<>() : new HashSet<>();
+
+ keys.add(key);
+
+ while (itr.hasNext()) {
+ key = itr.next();
+
+ keys.add(key);
+ }
+
+ return keys;
+ }
+
+ /** {@inheritDoc} */
+ @Override public Iterable<T> findAll(Iterable<ID> ids) {
+ return cache.getAll(toSet(ids)).values();
+ }
+
+ /** {@inheritDoc} */
+ @Override public long count() {
+ return cache.size(CachePeekMode.PRIMARY);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(ID id) {
+ cache.remove(id);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(T entity) {
+ throw new UnsupportedOperationException("Use IgniteRepository.delete(key) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public void delete(Iterable<? extends T> entities) {
+ throw new UnsupportedOperationException("Use IgniteRepository.deleteAll(keys) method instead.");
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAll(Iterable<ID> ids) {
+ cache.removeAll(toSet(ids));
+ }
+
+ /** {@inheritDoc} */
+ @Override public void deleteAll() {
+ cache.clear();
+ }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/package-info.java
new file mode 100644
index 0000000..749c9e7
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/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. -->
+ * Package contains supporting files required by Spring Data framework.
+ */
+
+package org.apache.ignite.springdata.repository.support;
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
new file mode 100644
index 0000000..62ff8c5
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.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.springdata;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.compoundkey.City;
+import org.apache.ignite.springdata.compoundkey.CityRepository;
+import org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+import static org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration.CLI_CONN_PORT;
+
+/**
+ * Test with using conpoud key in spring-data
+ * */
+public class IgniteSpringDataCompoundKeyTest extends GridCommonAbstractTest {
+ /** Application context */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** City repository */
+ private static CityRepository repo;
+
+ /** Cache name */
+ private static final String CACHE_NAME = "City";
+
+ /** Cities count */
+ private static final int TOTAL_COUNT = 5;
+
+ /** Count Afganistan cities */
+ private static final int AFG_COUNT = 4;
+
+ /** Quandahar identifier */
+ private static final int QUANDAHAR_ID = 2;
+
+ /** Afganistan county code */
+ private static final String AFG = "AFG";
+
+ /** test city Quandahar */
+ private static final City QUANDAHAR = new City("Qandahar","Qandahar", 237500);
+
+ /**
+ * Performs context initialization before tests.
+ */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+ ctx.register(CompoundKeyApplicationConfiguration.class);
+ ctx.refresh();
+
+ repo = ctx.getBean(CityRepository.class);
+ }
+
+ /**
+ * Load data
+ * */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ loadData();
+
+ assertEquals(TOTAL_COUNT, repo.count());
+ }
+
+ /**
+ * Performs context destroy after tests.
+ */
+ @Override protected void afterTestsStopped() {
+ ctx.close();
+ }
+
+ /** load data*/
+ public void loadData() throws Exception {
+ Ignite ignite = ctx.getBean(Ignite.class);
+
+ if (ignite.cacheNames().contains(CACHE_NAME))
+ ignite.destroyCache(CACHE_NAME);
+
+ try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:" + CLI_CONN_PORT + '/')) {
+ Statement st = conn.createStatement();
+
+ st.execute("DROP TABLE IF EXISTS City");
+ st.execute("CREATE TABLE City (ID INT, Name VARCHAR, CountryCode CHAR(3), District VARCHAR, Population INT, PRIMARY KEY (ID, CountryCode)) WITH \"template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, KEY_TYPE=org.apache.ignite.springdata.compoundkey.CityKey, VALUE_TYPE=org.apache.ignite.springdata.compoundkey.City\"");
+ st.execute("SET STREAMING ON;");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (1,'Kabul','AFG','Kabol',1780000)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (2,'Qandahar','AFG','Qandahar',237500)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (3,'Herat','AFG','Herat',186800)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (4,'Mazar-e-Sharif','AFG','Balkh',127800)");
+ st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (5,'Amsterdam','NLD','Noord-Holland',731200)");
+ }
+ }
+
+ /** Test */
+ @Test
+ public void test() {
+ assertEquals(AFG_COUNT, repo.findByCountryCode(AFG).size());
+ assertEquals(QUANDAHAR, repo.findById(QUANDAHAR_ID));
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
new file mode 100644
index 0000000..65f9239
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.springdata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.TreeSet;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.query.RunningQueryManager;
+import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonKey;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonRepositoryWithCompoundKey;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * CRUD tests.
+ */
+public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
+ /** Number of entries to store. */
+ private static final int CACHE_SIZE = 1000;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** Repository. */
+ private static PersonRepository repo;
+
+ /** Repository. */
+ private static PersonRepositoryWithCompoundKey repoWithCompoundKey;
+
+ /** */
+ @Rule
+ public final ExpectedException expected = ExpectedException.none();
+
+ /** */
+ private static IgniteEx ignite;
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+ ctx.register(ApplicationConfiguration.class);
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonRepository.class);
+ repoWithCompoundKey = ctx.getBean(PersonRepositoryWithCompoundKey.class);
+ ignite = ctx.getBean(IgniteEx.class);
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTest() throws Exception {
+ super.beforeTest();
+
+ fillInRepository();
+
+ assertEquals(CACHE_SIZE, repo.count());
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+
+ super.afterTest();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() {
+ ctx.destroy();
+ }
+
+ /** */
+ @Test
+ public void testPutGet() {
+ Person person = new Person("some_name", "some_surname");
+
+ int id = CACHE_SIZE + 1;
+
+ assertEquals(person, repo.save(id, person));
+
+ assertTrue(repo.exists(id));
+
+ assertEquals(person, repo.findOne(id));
+
+ expected.expect(UnsupportedOperationException.class);
+ expected.expectMessage("Use IgniteRepository.save(key,value) method instead.");
+ repo.save(person);
+ }
+
+ /** */
+ @Test
+ public void testPutAllGetAll() {
+ LinkedHashMap<Integer, Person> map = new LinkedHashMap<>();
+
+ for (int i = CACHE_SIZE; i < CACHE_SIZE + 50; i++)
+ map.put(i, new Person("some_name" + i, "some_surname" + i));
+
+ Iterator<Person> persons = repo.save(map).iterator();
+
+ assertEquals(CACHE_SIZE + 50, repo.count());
+
+ Iterator<Person> origPersons = map.values().iterator();
+
+ while (persons.hasNext())
+ assertEquals(origPersons.next(), persons.next());
+
+ expected.expect(UnsupportedOperationException.class);
+ expected.expectMessage("Use IgniteRepository.save(Map<keys,value>) method instead.");
+ repo.save(map.values());
+
+ persons = repo.findAll(map.keySet()).iterator();
+
+ int counter = 0;
+
+ while (persons.hasNext()) {
+ persons.next();
+ counter++;
+ }
+
+ assertEquals(map.size(), counter);
+ }
+
+ /** */
+ @Test
+ public void testGetAll() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ Iterator<Person> persons = repo.findAll().iterator();
+
+ int counter = 0;
+
+ while (persons.hasNext()) {
+ persons.next();
+ counter++;
+ }
+
+ assertEquals(repo.count(), counter);
+ }
+
+ /** */
+ @Test
+ public void testDelete() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ repo.delete(0);
+
+ assertEquals(CACHE_SIZE - 1, repo.count());
+ assertNull(repo.findOne(0));
+
+ expected.expect(UnsupportedOperationException.class);
+ expected.expectMessage("Use IgniteRepository.delete(key) method instead.");
+ repo.delete(new Person("", ""));
+ }
+
+ /**
+ *
+ */
+ @Test
+ public void testDeleteSet() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ TreeSet<Integer> ids = new TreeSet<>();
+
+ for (int i = 0; i < CACHE_SIZE / 2; i++)
+ ids.add(i);
+
+ expected.expect(UnsupportedOperationException.class);
+ expected.expectMessage("Use IgniteRepository.deleteAll(keys) method instead.");
+ repo.deleteAll(ids);
+
+ assertEquals(CACHE_SIZE / 2, repo.count());
+
+ ArrayList<Person> persons = new ArrayList<>();
+
+ for (int i = 0; i < 3; i++)
+ persons.add(new Person(String.valueOf(i), String.valueOf(i)));
+
+ repo.delete(persons);
+ }
+
+ /**
+ *
+ */
+ @Test
+ public void testDeleteAll() {
+ assertEquals(CACHE_SIZE, repo.count());
+
+ repo.deleteAll();
+
+ assertEquals(0, repo.count());
+ }
+
+ /** */
+ private void fillInRepository() {
+ for (int i = 0; i < CACHE_SIZE; i++)
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+
+ /** */
+ @Test
+ public void shouldDeleteAll() {
+ List<PersonKey> ids = prepareDataWithNonComparableKeys();
+
+ repoWithCompoundKey.deleteAll(ids);
+
+ assertEquals(0, repoWithCompoundKey.count());
+ }
+
+ /** */
+ @Test
+ public void shouldFindAll() {
+ List<PersonKey> ids = prepareDataWithNonComparableKeys();
+
+ Iterable<Person> res = repoWithCompoundKey.findAll(ids);
+
+ assertEquals(2, res.spliterator().estimateSize());
+ }
+
+ /** */
+ private List<PersonKey> prepareDataWithNonComparableKeys() {
+ List<PersonKey> ids = new ArrayList<>();
+
+ PersonKey key = new PersonKey(1, 1);
+ ids.add(key);
+
+ repoWithCompoundKey.save(key, new Person("test1", "test1"));
+
+ key = new PersonKey(2, 2);
+ ids.add(key);
+
+ repoWithCompoundKey.save(key, new Person("test2", "test2"));
+
+ assertEquals(2, repoWithCompoundKey.count());
+
+ return ids;
+ }
+
+ /** */
+ @Test
+ public void shouldNotLeakCursorsInRunningQueryManager() {
+ RunningQueryManager runningQryMgr = ((IgniteH2Indexing)ignite.context().query().getIndexing()).runningQueryManager();
+
+ assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+
+ List<Person> res = repo.simpleQuery("person0");
+
+ assertEquals(1, res.size());
+
+ assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+
+ Person person = repo.findTopBySecondNameStartingWith("lastName");
+
+ assertNotNull(person);
+
+ assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+
+ long cnt = repo.countByFirstName("person0");
+
+ assertEquals(1, cnt);
+
+ assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
new file mode 100644
index 0000000..51492c1
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.springdata;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonSecondRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+/**
+ *
+ */
+public class IgniteSpringDataQueriesSelfTest extends GridCommonAbstractTest {
+ /** Repository. */
+ private static PersonRepository repo;
+
+ /** Repository 2. */
+ private static PersonSecondRepository repo2;
+
+ /** Context. */
+ private static AnnotationConfigApplicationContext ctx;
+
+ /** Number of entries to store */
+ private static int CACHE_SIZE = 1000;
+
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ ctx = new AnnotationConfigApplicationContext();
+
+ ctx.register(ApplicationConfiguration.class);
+
+ ctx.refresh();
+
+ repo = ctx.getBean(PersonRepository.class);
+ repo2 = ctx.getBean(PersonSecondRepository.class);
+
+ for (int i = 0; i < CACHE_SIZE; i++)
+ repo.save(i, new Person("person" + Integer.toHexString(i),
+ "lastName" + Integer.toHexString((i + 16) % 256)));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTestsStopped() throws Exception {
+ ctx.destroy();
+ }
+
+ /** */
+ @Test
+ public void testExplicitQuery() {
+ List<Person> persons = repo.simpleQuery("person4a");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("person4a", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testEqualsPart() {
+ List<Person> persons = repo.findByFirstName("person4e");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertEquals("person4e", person.getFirstName());
+ }
+
+ /** */
+ @Test
+ public void testContainingPart() {
+ List<Person> persons = repo.findByFirstNameContaining("person4");
+
+ assertFalse(persons.isEmpty());
+
+ for (Person person : persons)
+ assertTrue(person.getFirstName().startsWith("person4"));
+ }
+
+ /** */
+ @Test
+ public void testTopPart() {
+ Iterable<Person> top = repo.findTopByFirstNameContaining("person4");
+
+ Iterator<Person> iter = top.iterator();
+
+ Person person = iter.next();
+
+ assertFalse(iter.hasNext());
+
+ assertTrue(person.getFirstName().startsWith("person4"));
+ }
+
+ /** */
+ @Test
+ public void testLikeAndLimit() {
+ Iterable<Person> like = repo.findFirst10ByFirstNameLike("person");
+
+ int cnt = 0;
+
+ for (Person next : like) {
+ assertTrue(next.getFirstName().contains("person"));
+
+ cnt++;
+ }
+
+ assertEquals(10, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCount() {
+ int cnt = repo.countByFirstNameLike("person");
+
+ assertEquals(1000, cnt);
+ }
+
+ /** */
+ @Test
+ public void testCount2() {
+ int cnt = repo.countByFirstNameLike("person4");
+
+ assertTrue(cnt < 1000);
+ }
+
+ /** */
+ @Test
+ public void testPageable() {
+ PageRequest pageable = new PageRequest(1, 5, Sort.Direction.DESC, "firstName");
+
+ HashSet<String> firstNames = new HashSet<>();
+
+ List<Person> pageable1 = repo.findByFirstNameRegex("^[a-z]+$", pageable);
+
+ assertEquals(5, pageable1.size());
+
+ for (Person person : pageable1) {
+ firstNames.add(person.getFirstName());
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+ }
+
+ List<Person> pageable2 = repo.findByFirstNameRegex("^[a-z]+$", pageable.next());
+
+ assertEquals(5, pageable2.size());
+
+ for (Person person : pageable2) {
+ firstNames.add(person.getFirstName());
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+ }
+
+ assertEquals(10, firstNames.size());
+ }
+
+ /** */
+ @Test
+ public void testAndAndOr() {
+ int cntAnd = repo.countByFirstNameLikeAndSecondNameLike("person1", "lastName1");
+
+ int cntOr = repo.countByFirstNameStartingWithOrSecondNameStartingWith("person1", "lastName1");
+
+ assertTrue(cntAnd <= cntOr);
+ }
+
+ /** */
+ @Test
+ public void testQueryWithSort() {
+ List<Person> persons = repo.queryWithSort("^[a-z]+$", new Sort(Sort.Direction.DESC, "secondName"));
+
+ Person previous = persons.get(0);
+
+ for (Person person : persons) {
+ assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+ previous = person;
+ }
+ }
+
+ /** */
+ @Test
+ public void testQueryWithPaging() {
+ List<Person> persons = repo.queryWithPageable("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+ assertEquals(7, persons.size());
+
+ Person previous = persons.get(0);
+
+ for (Person person : persons) {
+ assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+ assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+ previous = person;
+ }
+ }
+
+ /** */
+ @Test
+ public void testQueryFields() {
+ List<String> persons = repo.selectField("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+ assertEquals(7, persons.size());
+ }
+
+ /** */
+ @Test
+ public void testFindCacheEntries() {
+ List<Cache.Entry<Integer, Person>> cacheEntries = repo.findBySecondNameLike("stName1");
+
+ assertFalse(cacheEntries.isEmpty());
+
+ for (Cache.Entry<Integer, Person> entry : cacheEntries)
+ assertTrue(entry.getValue().getSecondName().contains("stName1"));
+ }
+
+ /** */
+ @Test
+ public void testFindOneCacheEntry() {
+ Cache.Entry<Integer, Person> cacheEntry = repo.findTopBySecondNameLike("tName18");
+
+ assertNotNull(cacheEntry);
+
+ assertTrue(cacheEntry.getValue().getSecondName().contains("tName18"));
+ }
+
+ /** */
+ @Test
+ public void testFindOneValue() {
+ Person person = repo.findTopBySecondNameStartingWith("lastName18");
+
+ assertNotNull(person);
+
+ assertTrue(person.getSecondName().startsWith("lastName18"));
+ }
+
+ /** */
+ @Test
+ public void testSelectSeveralFields() {
+ List<List> lists = repo.selectSeveralField("^[a-z]+$", new PageRequest(2, 6));
+
+ assertEquals(6, lists.size());
+
+ for (List list : lists) {
+ assertEquals(2, list.size());
+
+ assertTrue(list.get(0) instanceof Integer);
+ }
+ }
+
+ /** */
+ @Test
+ public void testCountQuery() {
+ int cnt = repo.countQuery(".*");
+
+ assertEquals(256, cnt);
+ }
+
+ /** */
+ @Test
+ public void testSliceOfCacheEntries() {
+ Slice<Cache.Entry<Integer, Person>> slice = repo2.findBySecondNameIsNot("lastName18", new PageRequest(3, 4));
+
+ assertEquals(4, slice.getSize());
+
+ for (Cache.Entry<Integer, Person> entry : slice)
+ assertFalse("lastName18".equals(entry.getValue().getSecondName()));
+ }
+
+ /** */
+ @Test
+ public void testSliceOfLists() {
+ Slice<List> lists = repo2.querySliceOfList("^[a-z]+$", new PageRequest(0, 3));
+
+ assertEquals(3, lists.getSize());
+
+ for (List list : lists) {
+ assertEquals(2, list.size());
+
+ assertTrue(list.get(0) instanceof Integer);
+ }
+ }
+
+ /**
+ * Tests the repository method with a custom query which takes no parameters.
+ */
+ @Test
+ public void testCountAllPersons() {
+ int cnt = repo.countAllPersons();
+
+ assertEquals(CACHE_SIZE, cnt);
+ }
+}
+
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
new file mode 100644
index 0000000..e86a575
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.Objects;
+
+/**
+ * Value-class
+ * */
+public class City {
+ /** City name */
+ private String name;
+
+ /** City district */
+ private String district;
+
+ /** City population */
+ private int population;
+
+ /**
+ * @param name city name
+ * @param district city district
+ * @param population city population
+ * */
+ public City(String name, String district, int population) {
+ this.name = name;
+ this.district = district;
+ this.population = population;
+ }
+
+ /**
+ * @return city name
+ * */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name city name
+ * */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return city district
+ * */
+ public String getDistrict() {
+ return district;
+ }
+
+ /**
+ * @param district city district
+ * */
+ public void setDistrict(String district) {
+ this.district = district;
+ }
+
+ /**
+ * @return city population
+ * */
+ public int getPopulation() {
+ return population;
+ }
+
+ /**
+ * @param population city population
+ * */
+ public void setPopulation(int population) {
+ this.population = population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return name + " | " + district + " | " + population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ City city = (City)o;
+
+ return
+ Objects.equals(this.name, city.name) &&
+ Objects.equals(this.district, city.district) &&
+ this.population == city.population;
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(name, district, population);
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
new file mode 100644
index 0000000..88226fe
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
@@ -0,0 +1,79 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/** Compound key for city class */
+public class CityKey implements Serializable {
+ /** city identifier */
+ private int ID;
+
+ /** affinity key countrycode */
+ @AffinityKeyMapped
+ private String COUNTRYCODE;
+
+ /**
+ * @param id city identifier
+ * @param countryCode city countrycode
+ * */
+ public CityKey(int id, String countryCode) {
+ this.ID = id;
+ this.COUNTRYCODE = countryCode;
+ }
+
+ /**
+ * @return city id
+ * */
+ public int getId() {
+ return ID;
+ }
+
+ /**
+ * @return countrycode
+ * */
+ public String getCountryCode() {
+ return COUNTRYCODE;
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ CityKey key = (CityKey)o;
+
+ return ID == key.ID &&
+ COUNTRYCODE.equals(key.COUNTRYCODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(ID, COUNTRYCODE);
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return ID + " | " + COUNTRYCODE;
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
new file mode 100644
index 0000000..691fe8e
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.springdata.compoundkey;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.springframework.stereotype.Repository;
+
+/** City repository */
+@Repository
+@RepositoryConfig(cacheName = "City")
+public interface CityRepository extends IgniteRepository<City, CityKey> {
+ /**
+ * Find city by id
+ * @param id city identifier
+ * @return city
+ * */
+ public City findById(int id);
+
+ /**
+ * Find all cities by coutrycode
+ * @param cc coutrycode
+ * @return list of cache enrties CityKey -> City
+ * */
+ public List<Cache.Entry<CityKey, City>> findByCountryCode(String cc);
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
new file mode 100644
index 0000000..7701f3d
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.springdata.compoundkey;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring application configuration
+ * */
+@Configuration
+@EnableIgniteRepositories
+public class CompoundKeyApplicationConfiguration {
+ /** */
+ public static final int CLI_CONN_PORT = 10810;
+
+ /**
+ * Ignite instance bean
+ * */
+ @Bean
+ public Ignite igniteInstance() {
+ return Ignition.start(new IgniteConfiguration()
+ .setClientConnectorConfiguration(new ClientConnectorConfiguration().setPort(CLI_CONN_PORT)));
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
new file mode 100644
index 0000000..ff1045e
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.springdata.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ *
+ */
+@Configuration
+@EnableIgniteRepositories
+public class ApplicationConfiguration {
+ /**
+ * @return Ignite instance.
+ */
+ @Bean
+ public Ignite igniteInstance() {
+ IgniteConfiguration cfg = new IgniteConfiguration()
+ .setCacheConfiguration(
+ new CacheConfiguration<Integer, Person>("PersonCache")
+ .setIndexedTypes(Integer.class, Person.class),
+ new CacheConfiguration<PersonKey, Person>("PersonWithKeyCache")
+ )
+ .setDiscoverySpi(new TcpDiscoverySpi().setIpFinder(new TcpDiscoveryVmIpFinder(true)));
+
+ return Ignition.start(cfg);
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
new file mode 100644
index 0000000..154937f
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/Person.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.springdata.misc;
+
+import java.util.Objects;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+
+/**
+ * DTO class.
+ */
+public class Person {
+ /** First name. */
+ @QuerySqlField(index = true)
+ private String firstName;
+
+ /** Second name. */
+ @QuerySqlField(index = true)
+ private String secondName;
+
+ /**
+ * @param firstName First name.
+ * @param secondName Second name.
+ */
+ public Person(String firstName, String secondName) {
+ this.firstName = firstName;
+ this.secondName = secondName;
+ }
+
+ /**
+ * @return First name.
+ */
+ public String getFirstName() {
+ return firstName;
+ }
+
+ /**
+ * @param firstName First name.
+ */
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ /**
+ * @return Second name.
+ */
+ public String getSecondName() {
+ return secondName;
+ }
+
+ /**
+ * @param secondName Second name.
+ */
+ public void setSecondName(String secondName) {
+ this.secondName = secondName;
+ }
+
+ /** {@inheritDoc} */
+ @Override public String toString() {
+ return "Person{" +
+ "firstName='" + firstName + '\'' +
+ ", secondName='" + secondName + '\'' +
+ '}';
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean equals(Object o) {
+ if (this == o)
+ return true;
+
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ Person person = (Person)o;
+
+ return Objects.equals(firstName, person.firstName) &&
+ Objects.equals(secondName, person.secondName);
+ }
+
+ /** {@inheritDoc} */
+ @Override public int hashCode() {
+ return Objects.hash(firstName, secondName);
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
new file mode 100644
index 0000000..2537c57
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
@@ -0,0 +1,54 @@
+/*
+ * 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.springdata.misc;
+
+import java.io.Serializable;
+
+/**
+ * Compound key.
+ */
+public class PersonKey implements Serializable {
+ /** */
+ private int id1;
+
+ /** */
+ private int id2;
+
+ /**
+ * @param id1 ID1.
+ * @param id2 ID2.
+ */
+ public PersonKey(int id1, int id2) {
+ this.id1 = id1;
+ this.id2 = id2;
+ }
+
+ /**
+ * @return ID1
+ */
+ public int getId1() {
+ return id1;
+ }
+
+ /**
+ * @return ID1
+ */
+ public int getId2() {
+ return id1;
+ }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
new file mode 100644
index 0000000..d8444d1
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -0,0 +1,99 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.Query;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Integer> {
+ /** */
+ public List<Person> findByFirstName(String val);
+
+ /** */
+ public List<Person> findByFirstNameContaining(String val);
+
+ /** */
+ public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+ /** */
+ public Collection<Person> findTopByFirstNameContaining(String val);
+
+ /** */
+ public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstName(String val);
+
+ /** */
+ public int countByFirstNameLike(String val);
+
+ /** */
+ public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+ /** */
+ public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+ /** */
+ public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+ /** */
+ public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+ /** */
+ public Person findTopBySecondNameStartingWith(String val);
+
+ /** */
+ @Query("firstName = ?")
+ public List<Person> simpleQuery(String val);
+
+ /** */
+ @Query("firstName REGEXP ?")
+ public List<Person> queryWithSort(String val, Sort sort);
+
+ /** */
+ @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+ public List<Person> queryWithPageable(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+ public List<String> selectField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public List<List> selectSeveralField(String val, Pageable pageable);
+
+ /** */
+ @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+ public int countQuery(String val);
+
+ /** */
+ @Query("SELECT count(*) FROM Person")
+ public int countAllPersons();
+}
+
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
new file mode 100644
index 0000000..e868911
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
@@ -0,0 +1,28 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonWithKeyCache")
+public interface PersonRepositoryWithCompoundKey extends IgniteRepository<Person, PersonKey> {
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
new file mode 100644
index 0000000..a82e822
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
@@ -0,0 +1,40 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.Query;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.springframework.data.domain.AbstractPageRequest;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+
+/**
+ *
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonSecondRepository extends IgniteRepository<Person, Integer> {
+ /** */
+ public Slice<Cache.Entry<Integer, Person>> findBySecondNameIsNot(String val, PageRequest pageReq);
+
+ /** */
+ @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+ public Slice<List> querySliceOfList(String val, AbstractPageRequest pageReq);
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringDataTestSuite.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringDataTestSuite.java
new file mode 100644
index 0000000..cc063a6
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringDataTestSuite.java
@@ -0,0 +1,37 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.springdata.IgniteSpringDataCompoundKeyTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfTest;
+import org.apache.ignite.springdata.IgniteSpringDataQueriesSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Spring Data test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ IgniteSpringDataCrudSelfTest.class,
+ IgniteSpringDataQueriesSelfTest.class,
+ IgniteSpringDataCompoundKeyTest.class
+})
+public class IgniteSpringDataTestSuite {
+}
+
diff --git a/parent/pom.xml b/parent/pom.xml
index 6444b27..d4c69fa 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -126,10 +126,12 @@
<snappy.version>1.1.7.2</snappy.version>
<spark.hadoop.version>2.6.5</spark.hadoop.version>
<spark.version>2.3.0</spark.version>
- <spring.data.version>1.13.14.RELEASE</spring.data.version> <!-- don't forget to update spring version -->
- <spring.version>4.3.18.RELEASE</spring.version><!-- don't forget to update spring-data version -->
- <spring.data-2.0.version>2.0.9.RELEASE</spring.data-2.0.version> <!-- don't forget to update spring-5.0 version -->
- <spring-5.0.version>5.0.8.RELEASE</spring-5.0.version><!-- don't forget to update spring-data-2.0 version -->
+ <spring.data.version>1.13.23.RELEASE</spring.data.version> <!-- don't forget to update spring version -->
+ <spring.version>4.3.26.RELEASE</spring.version><!-- don't forget to update spring-data version -->
+ <spring.data-2.0.version>2.0.13.RELEASE</spring.data-2.0.version> <!-- don't forget to update spring-5.0 version -->
+ <spring-5.0.version>5.0.16.RELEASE</spring-5.0.version><!-- don't forget to update spring-data-2.0 version -->
+ <spring.data-2.2.version>2.2.3.RELEASE</spring.data-2.2.version> <!-- don't forget to update spring-5.2 version -->
+ <spring-5.2.version>5.2.3.RELEASE</spring-5.2.version><!-- don't forget to update spring-data-2.2 version -->
<spring41.osgi.feature.version>4.1.7.RELEASE_1</spring41.osgi.feature.version>
<spring-boot.version>2.2.2.RELEASE</spring-boot.version>
<storm.version>1.1.1</storm.version>
@@ -234,7 +236,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
- <version>4.11</version>
+ <version>4.12</version>
<scope>test</scope>
</dependency>
@@ -346,6 +348,10 @@
<packages>org.apache.ignite.springdata20.repository*</packages>
</group>
<group>
+ <title>SpringData 2.2 integration</title>
+ <packages>org.apache.ignite.springdata22.repository*</packages>
+ </group>
+ <group>
<title>RocketMQ integration</title>
<packages>org.apache.ignite.stream.rocketmq*</packages>
</group>
@@ -722,6 +728,14 @@
<exclude>**/keystore/ca/*.txt.attr</exclude><!--auto generated files-->
<exclude>**/keystore/ca/*serial</exclude><!--auto generated files-->
<exclude>**/META-INF/services/**</exclude> <!-- Interface mappings: cannot be changed -->
+ <!-- spring data 2.X modules, borrowed code from spring-jpa 5.2.0, Apache 2.0 licenses -->
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/QueryUtils.java</exclude>
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/EmptyDeclaredQuery.java</exclude>
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/ExpressionBasedStringQuery.java</exclude>
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/spel/SpelEvaluator.java</exclude>
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/spel/SpelQueryContext.java</exclude>
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/DeclaredQuery.java</exclude>
+ <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/StringQuery.java</exclude>
<!--special excludes-->
<exclude>idea/ignite_codeStyle.xml</exclude>
<exclude>**/DEVNOTES*.txt</exclude>
diff --git a/pom.xml b/pom.xml
index 55d3af5..a3232d1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,9 @@
<module>modules/camel-ext</module>
<module>modules/jms11-ext</module>
<module>modules/kafka-ext</module>
+ <module>modules/spring-data-ext</module>
+ <module>modules/spring-data-2.0-ext</module>
+ <module>modules/spring-data-2.2-ext</module>
</modules>
<profiles>
@@ -124,6 +127,18 @@
</goals>
<phase>validate</phase>
<configuration>
+ <additionalDependencies>
+ <!--
+ Only the last version of spring data is included to class path, some classes
+ required by old spring data modules are absent in this version.
+ Add spring-data-2.0 explicitly to be able to find all required classes.
+ -->
+ <dependency>
+ <groupId>org.springframework.data</groupId>
+ <artifactId>spring-data-commons</artifactId>
+ <version>${spring.data-2.0.version}</version>
+ </dependency>
+ </additionalDependencies>
<reportOutputDirectory>${basedir}/target/javadoc</reportOutputDirectory>
<destDir>core</destDir>
<subpackages>org.apache.ignite -exclude org.apache.ignite.codegen:org.apache.ignite.examples:org.apache.ignite.internal:org.apache.ignite.schema:org.apache.ignite.tests:org.apache.ignite.tools:org.apache.ignite.util:org.apache.ignite.spi.discovery.tcp.messages:org.apache.ignite.spi.discovery.tcp.internal:org.apache.ignite.spi.communication.tcp.internal:org.apache.ignite.spi.discovery.zk.internal:org.apache.ignite.spi.deployment.uri.scanners:org.apache.ignite.spi.deployment.uri.tasks:org.apache.ignite.yardstick:org.apache.ignite.webtest</subpackages>