diff --git a/pom.xml b/pom.xml
index 767854e32..55bf6f32c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,5 +1,5 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.data.examples
@@ -16,6 +16,7 @@
+ util
bom
couchbase
elasticsearch
@@ -36,6 +37,7 @@
1.1.3
1.18.0
+ 1.9.1
diff --git a/r2dbc/example/pom.xml b/r2dbc/example/pom.xml
index edf96a7a7..4b4abbe2b 100644
--- a/r2dbc/example/pom.xml
+++ b/r2dbc/example/pom.xml
@@ -13,4 +13,11 @@
Spring Data R2DBC - Example
+
+
+ org.springframework.data.examples
+ spring-data-examples-utils
+ ${project.version}
+
+
diff --git a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java
index b80692916..a83edae39 100644
--- a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java
+++ b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/CustomerRepositoryIntegrationTests.java
@@ -15,6 +15,7 @@
*/
package example.springdata.r2dbc.basics;
+import example.springdata.test.util.InfrastructureRule;
import reactor.core.publisher.Hooks;
import reactor.test.StepVerifier;
@@ -23,6 +24,7 @@
import java.util.List;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -31,7 +33,10 @@
import org.springframework.test.context.junit4.SpringRunner;
/**
+ * Tests demonstrating the use of R2DBC.
+ *
* @author Oliver Gierke
+ * @author Jens Schauder
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = InfrastructureConfiguration.class)
@@ -40,6 +45,8 @@ public class CustomerRepositoryIntegrationTests {
@Autowired CustomerRepository customers;
@Autowired DatabaseClient database;
+ @Rule @Autowired public InfrastructureRule requiresPostgres;
+
@Before
public void setUp() {
diff --git a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java
index 94e29bd6b..9b0e9fda0 100644
--- a/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java
+++ b/r2dbc/example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java
@@ -15,18 +15,19 @@
*/
package example.springdata.r2dbc.basics;
+import example.springdata.test.util.InfrastructureRule;
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
+import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.testcontainers.containers.PostgreSQLContainer;
-
-import javax.annotation.PreDestroy;
+import reactor.test.StepVerifier;
/**
* @author Oliver Gierke
@@ -34,7 +35,14 @@
@Configuration
class InfrastructureConfiguration {
- private PostgreSQLContainer postgres = new PostgreSQLContainer();
+ @Bean
+ InfrastructureRule infrastructureRule() {
+
+ return new InfrastructureRule<>( //
+ this::checkForlLocalPostgres, //
+ this::startPostgresInDocker //
+ );
+ }
@Bean
CustomerRepository customerRepository(R2dbcRepositoryFactory factory) {
@@ -61,10 +69,16 @@ DatabaseClient databaseClient(ConnectionFactory factory) {
@Bean
PostgresqlConnectionFactory connectionFactory() {
+ return new PostgresqlConnectionFactory(infrastructureRule().getInfo());
+ }
+
+ @NotNull
+ private InfrastructureRule.InfrastructureInfo startPostgresInDocker() {
+ PostgreSQLContainer postgres = new PostgreSQLContainer();
postgres.start();
- PostgresqlConnectionConfiguration config = PostgresqlConnectionConfiguration.builder() //
+ PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() //
.host(postgres.getContainerIpAddress()) //
.port(postgres.getFirstMappedPort()) //
.database(postgres.getDatabaseName()) //
@@ -72,11 +86,33 @@ PostgresqlConnectionFactory connectionFactory() {
.password(postgres.getPassword()) //
.build();
- return new PostgresqlConnectionFactory(config);
+ return new InfrastructureRule.InfrastructureInfo<>(true, configuration, null, postgres::stop);
}
- @PreDestroy
- void shutdown() {
- postgres.stop();
+ @NotNull
+ private InfrastructureRule.InfrastructureInfo checkForlLocalPostgres() {
+
+ PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() //
+ .host("localhost") //
+ .port(5432) //
+ .database("postgres") //
+ .username("postgres") //
+ .password("") //
+ .build();
+
+ try {
+
+ new PostgresqlConnectionFactory(configuration).create()
+ .as(StepVerifier::create) //
+ .assertNext(c -> {
+ }) //
+ .verifyComplete();
+ } catch (AssertionError re) {
+
+ return new InfrastructureRule.InfrastructureInfo<>(false, null, re, () ->{});
+ }
+
+ return new InfrastructureRule.InfrastructureInfo<>(true, configuration, null, () ->{});
}
+
}
diff --git a/solr/example/pom.xml b/solr/example/pom.xml
index 1028e6d89..cb3ebfecd 100644
--- a/solr/example/pom.xml
+++ b/solr/example/pom.xml
@@ -18,6 +18,17 @@
${project.version}
test
+
+ org.springframework.data.examples
+ spring-data-examples-utils
+ ${project.version}
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+ test
+
diff --git a/solr/example/src/main/java/example/springdata/solr/product/Product.java b/solr/example/src/main/java/example/springdata/solr/product/Product.java
index cc22ea1a9..e04af7d64 100644
--- a/solr/example/src/main/java/example/springdata/solr/product/Product.java
+++ b/solr/example/src/main/java/example/springdata/solr/product/Product.java
@@ -43,7 +43,7 @@ public class Product {
private @Indexed(name = "cat") List category;
private @Indexed(name = "store") Point location;
private @Indexed String description;
- private @Indexed boolean inStock;
+ private @Indexed Boolean inStock;
private @Indexed Integer popularity;
private @Score Float score;
}
diff --git a/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java b/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java
index 0a4ec5c27..29ba05b81 100644
--- a/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java
+++ b/solr/example/src/test/java/example/springdata/solr/AdvancedSolrRepositoryTests.java
@@ -22,20 +22,20 @@
import example.springdata.solr.product.Product;
import example.springdata.solr.product.ProductRepository;
-import example.springdata.solr.test.util.RequiresSolrServer;
+import example.springdata.test.util.InfrastructureRule;
import java.time.Duration;
import java.util.Arrays;
import java.util.Optional;
-import org.junit.ClassRule;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.PageRequest;
-import org.springframework.data.repository.CrudRepository;
import org.springframework.data.solr.core.SolrOperations;
import org.springframework.data.solr.core.query.Function;
import org.springframework.data.solr.core.query.Query;
@@ -48,33 +48,41 @@
* @author Christoph Strobl
* @author Oliver Gierke
* @author Mark Paluch
+ * @author Jens Schauder
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class AdvancedSolrRepositoryTests {
- public static @ClassRule RequiresSolrServer requiresRunningServer = RequiresSolrServer.onLocalhost();
+ @Rule @Autowired public InfrastructureRule requiresRunningServer;
- @Configuration
- static class Config extends SolrTestConfiguration {
-
- @Override
- protected void doInitTestData(CrudRepository repository) {
+ @Autowired ProductRepository repository;
+ @Autowired SolrOperations operations;
- Product playstation = Product.builder().id("id-1").name("Playstation")
- .description("The Sony playstation was the top selling gaming system in 1994.").popularity(5).build();
- Product playstation2 = Product.builder().id("id-2").name("Playstation Two")
- .description("Playstation two is the successor of playstation in 2000.").build();
- Product superNES = Product.builder().id("id-3").name("Super Nintendo").popularity(3).build();
- Product nintendo64 = Product.builder().id("id-4").name("N64").description("Nintendo 64").popularity(2).build();
- repository.saveAll(Arrays.asList(playstation, playstation2, superNES, nintendo64));
- }
+ /**
+ * Remove test data when context is shut down.
+ */
+ public @After void deleteDocumentsOnShutdown() {
+ repository.deleteAll();
}
- @Autowired ProductRepository repository;
- @Autowired SolrOperations operations;
+ /**
+ * Initialize Solr instance with test data once context has started.
+ */
+ public @Before void initWithTestData() throws InterruptedException {
+ repository.deleteAll();
+
+ Product playstation = Product.builder().id("id-1").name("Playstation")
+ .description("The Sony playstation was the top selling gaming system in 1994.").popularity(5).build();
+ Product playstation2 = Product.builder().id("id-2").name("Playstation Two")
+ .description("Playstation two is the successor of playstation in 2000.").build();
+ Product superNES = Product.builder().id("id-3").name("Super Nintendo").popularity(3).build();
+ Product nintendo64 = Product.builder().id("id-4").name("N64").description("Nintendo 64").popularity(2).build();
+
+ repository.saveAll(Arrays.asList(playstation, playstation2, superNES, nintendo64));
+ }
/**
* {@link HighlightPage} holds next to the entities found also information about where a match was found within the
* document. This allows to fine grained display snipplets of data containing the matching term in context.
diff --git a/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java b/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java
index e06293b7e..84db2d0a6 100644
--- a/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java
+++ b/solr/example/src/test/java/example/springdata/solr/BasicSolrRepositoryTests.java
@@ -15,10 +15,15 @@
*/
package example.springdata.solr;
+import example.springdata.solr.product.Product;
import example.springdata.solr.product.ProductRepository;
-import example.springdata.solr.test.util.RequiresSolrServer;
+import example.springdata.test.util.InfrastructureRule;
-import org.junit.ClassRule;
+import java.util.stream.IntStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -27,15 +32,36 @@
/**
* @author Christoph Strobl
+ * @author Jens Schauder
*/
@RunWith(SpringRunner.class)
-@SpringBootTest(classes = SolrTestConfiguration.class)
+@SpringBootTest
public class BasicSolrRepositoryTests {
- public static @ClassRule RequiresSolrServer requiresRunningServer = RequiresSolrServer.onLocalhost();
+
+
+ @Rule @Autowired public InfrastructureRule requiresRunningServer;
@Autowired ProductRepository repository;
+ /**
+ * Remove test data when context is shut down.
+ */
+ public @After void deleteDocumentsOnShutdown() {
+ repository.deleteAll();
+ }
+
+ /**
+ * Initialize Solr instance with test data once context has started.
+ */
+ public @Before void initWithTestData() {
+
+ repository.deleteAll();
+
+ IntStream.range(0, 100)
+ .forEach(index -> repository.save(Product.builder().id("p-" + index).name("foobar").build()));
+ }
+
/**
* Finds all entries using a single request.
*/
diff --git a/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java b/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java
index 079c81a28..186619a32 100644
--- a/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java
+++ b/solr/example/src/test/java/example/springdata/solr/SolrTestConfiguration.java
@@ -16,13 +16,12 @@
package example.springdata.solr;
import example.springdata.solr.product.Product;
-
-import java.util.stream.IntStream;
-
-import javax.annotation.PostConstruct;
-import javax.annotation.PreDestroy;
+import example.springdata.solr.test.util.SolrInfrastructureRule;
+import example.springdata.test.util.InfrastructureRule;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@@ -32,33 +31,22 @@
/**
* @author Christoph Strobl
* @author Oliver Gierke
+ * @author Jens Schauder
*/
@SpringBootApplication
public class SolrTestConfiguration {
@Autowired CrudRepository repo;
- public @Bean SolrTemplate solrTemplate() {
- return new SolrTemplate(new HttpSolrClient.Builder().withBaseSolrUrl("http://localhost:8983/solr").build());
- }
+ private static final Logger LOG = LoggerFactory.getLogger(SolrTestConfiguration.class);
- /**
- * Remove test data when context is shut down.
- */
- public @PreDestroy void deleteDocumentsOnShutdown() {
- repo.deleteAll();
- }
+ public @Bean InfrastructureRule infrastructureRule() {
- /**
- * Initialize Solr instance with test data once context has started.
- */
- public @PostConstruct void initWithTestData() {
- doInitTestData(repo);
+ return new SolrInfrastructureRule("techproducts");
}
- protected void doInitTestData(CrudRepository repository) {
-
- IntStream.range(0, 100)
- .forEach(index -> repository.save(Product.builder().id("p-" + index).name("foobar").build()));
+ public @Bean SolrTemplate solrTemplate() {
+ return new SolrTemplate(new HttpSolrClient.Builder().withBaseSolrUrl(infrastructureRule().getInfo()).build());
}
+
}
diff --git a/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java b/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java
index f4ce8e79f..866106e77 100644
--- a/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java
+++ b/solr/managed-schema/src/test/java/example/springdata/solr/SolrRepositoryTests.java
@@ -17,9 +17,9 @@
import example.springdata.solr.product.ManagedProduct;
import example.springdata.solr.product.ProductRepository;
-import example.springdata.solr.test.util.RequiresSolrServer;
+import example.springdata.test.util.InfrastructureRule;
-import org.junit.ClassRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -28,12 +28,13 @@
/**
* @author Christoph Strobl
+ * @author Jens Schauder
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SolrRepositoryTests {
- public static @ClassRule RequiresSolrServer requiresRunningServer = RequiresSolrServer.onLocalhost();
+ @Rule @Autowired public InfrastructureRule requiresRunningServer;
@Autowired ProductRepository repo;
diff --git a/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java b/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java
index f7fd03966..7c2f8bc65 100644
--- a/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java
+++ b/solr/managed-schema/src/test/java/example/springdata/solr/SolrTestConfiguration.java
@@ -19,6 +19,8 @@
import javax.annotation.PreDestroy;
+import example.springdata.solr.test.util.SolrInfrastructureRule;
+import example.springdata.test.util.InfrastructureRule;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,8 +43,13 @@ public class SolrTestConfiguration {
@Autowired ProductRepository repo;
+ public @Bean
+ InfrastructureRule infrastructureRule() {
+ return new SolrInfrastructureRule("schemaless");
+ }
+
public @Bean SolrClient solrClient() {
- return new HttpSolrClient.Builder().withBaseSolrUrl("http://localhost:8983/solr").build();
+ return new HttpSolrClient.Builder().withBaseSolrUrl(infrastructureRule().getInfo()).build();
}
/**
diff --git a/solr/util/pom.xml b/solr/util/pom.xml
index 9be802f0d..6d7c6ea99 100644
--- a/solr/util/pom.xml
+++ b/solr/util/pom.xml
@@ -16,5 +16,15 @@
junit
junit
+
+ org.testcontainers
+ testcontainers
+ 1.7.3
+
+
+ org.springframework.data.examples
+ spring-data-examples-utils
+ ${project.version}
+
diff --git a/solr/util/src/main/java/example/springdata/solr/test/util/RequiresSolrServer.java b/solr/util/src/main/java/example/springdata/solr/test/util/RequiresSolrServer.java
deleted file mode 100644
index ee36bfd29..000000000
--- a/solr/util/src/main/java/example/springdata/solr/test/util/RequiresSolrServer.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2014-2018 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 example.springdata.solr.test.util;
-
-import java.io.IOException;
-
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.hamcrest.core.Is;
-import org.junit.Assume;
-import org.junit.AssumptionViolatedException;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * {@link TestRule} implementation using {@link CloseableHttpClient} to check if Solr is running by sending
- * {@literal GET} request to {@literal /admin/info/system}.
- *
- * @author Christoph Strobl
- */
-public class RequiresSolrServer implements TestRule {
-
- private static final String PING_PATH = "/admin/info/system";
-
- private final String baseUrl;
-
- private RequiresSolrServer(String baseUrl) {
- this.baseUrl = baseUrl;
- }
-
- public static RequiresSolrServer onLocalhost() {
- return new RequiresSolrServer("http://localhost:8983/solr");
- }
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new Statement() {
-
- @Override
- public void evaluate() throws Throwable {
-
- checkServerRunning();
- base.evaluate();
- }
- };
- }
-
- private void checkServerRunning() {
-
- try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
- CloseableHttpResponse response = client.execute(new HttpGet(baseUrl + PING_PATH));
- if (response != null && response.getStatusLine() != null) {
- Assume.assumeThat(response.getStatusLine().getStatusCode(), Is.is(200));
- }
- } catch (IOException e) {
- throw new AssumptionViolatedException("SolrServer does not seem to be running", e);
- }
- }
-
-}
diff --git a/solr/util/src/main/java/example/springdata/solr/test/util/SolrExampleContainer.java b/solr/util/src/main/java/example/springdata/solr/test/util/SolrExampleContainer.java
new file mode 100644
index 000000000..859e05a25
--- /dev/null
+++ b/solr/util/src/main/java/example/springdata/solr/test/util/SolrExampleContainer.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018 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 example.springdata.solr.test.util;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.testcontainers.containers.GenericContainer;
+
+/**
+ * @author Jens Schauder
+ */
+public class SolrExampleContainer extends GenericContainer {
+
+ private static final String IMAGE_NAME = "solr";
+ private static final String DEFAULT_VERSION = "7";
+ private static final int DEFAULT_PORT = 8983;
+ private final String example;
+
+ public SolrExampleContainer(final String example) {
+
+ super(IMAGE_NAME + ":" + DEFAULT_VERSION);
+ this.example = example;
+ this.withExposedPorts(DEFAULT_PORT) //
+ .withCommand("bash", "-c", "solr start -e " + example + " && tail -f /dev/null");
+
+ }
+
+ @Override
+ public void start() {
+ super.start();
+
+ Integer port = getMappedPort(DEFAULT_PORT);
+ String url = "http://localhost:" + port + "/" + IMAGE_NAME;
+
+ waitForSolr(url);
+
+ }
+
+ private void waitForSolr(String url) {
+
+ long waitStart = System.currentTimeMillis();
+ long maxWaitDuration = 10000;
+
+ do {
+
+ try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
+
+ CloseableHttpResponse response = client.execute(new HttpGet(url + "/" + example + "/admin/ping"));
+ if (response != null && response.getStatusLine() != null && response.getStatusLine().getStatusCode() == 200) {
+
+ break;
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ } while (System.currentTimeMillis() < waitStart + maxWaitDuration);
+ }
+
+ public String getSolrBaseUrl() {
+ return "http://localhost:" + getMappedPort(8983) + "/solr";
+ }
+}
diff --git a/solr/util/src/main/java/example/springdata/solr/test/util/SolrInfrastructureRule.java b/solr/util/src/main/java/example/springdata/solr/test/util/SolrInfrastructureRule.java
new file mode 100644
index 000000000..06fe99394
--- /dev/null
+++ b/solr/util/src/main/java/example/springdata/solr/test/util/SolrInfrastructureRule.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 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 example.springdata.solr.test.util;
+
+import example.springdata.test.util.InfrastructureRule;
+
+import java.io.IOException;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.hamcrest.core.Is;
+import org.junit.Assume;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+
+/**
+ * A JUnit Rule to locate or create a Solr instance including one of the sets of example data that is part of the Solr distribution.
+ *
+ * @author Jens Schauder
+ */
+public class SolrInfrastructureRule extends InfrastructureRule {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SolrInfrastructureRule.class);
+
+ public SolrInfrastructureRule(String example) {
+ super(() -> checkNativeInstance("http://localhost:8983/solr", "/admin/info/system"),
+ () -> tryToStartInstanceInDocker(example));
+
+ }
+
+ private static InfrastructureInfo checkNativeInstance(String baseUrl, String path) {
+
+ try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
+
+ CloseableHttpResponse response = client.execute(new HttpGet(baseUrl + path));
+ if (response != null && response.getStatusLine() != null) {
+ Assume.assumeThat(response.getStatusLine().getStatusCode(), Is.is(200));
+ }
+
+ return new InfrastructureInfo<>(true, baseUrl, null, () -> {});
+ } catch (IOException e) {
+ return new InfrastructureInfo<>(false, null, e, () -> {});
+ }
+ }
+
+ private static InfrastructureInfo tryToStartInstanceInDocker(String example) {
+
+ SolrExampleContainer solrContainer = //
+ new SolrExampleContainer(example) //
+ .withLogConsumer(new Slf4jLogConsumer(LOG));
+
+ solrContainer.start();
+
+ return new InfrastructureInfo<>(true, solrContainer.getSolrBaseUrl(), null, () -> solrContainer.stop());
+ }
+
+}
diff --git a/solr/util/src/test/java/example/springdata/solr/test/util/SolrExampleContainerTests.java b/solr/util/src/test/java/example/springdata/solr/test/util/SolrExampleContainerTests.java
new file mode 100644
index 000000000..4ffef84dd
--- /dev/null
+++ b/solr/util/src/test/java/example/springdata/solr/test/util/SolrExampleContainerTests.java
@@ -0,0 +1,17 @@
+package example.springdata.solr.test.util;
+
+import org.junit.Test ;
+
+/**
+ * @author Jens Schauder
+ */
+public class SolrExampleContainerTests {
+
+ @Test
+ public void runsWithoutException() {
+
+ SolrExampleContainer container = new SolrExampleContainer("techproducts");
+ container.stop();
+ }
+
+}
diff --git a/util/pom.xml b/util/pom.xml
new file mode 100644
index 000000000..658bfbec8
--- /dev/null
+++ b/util/pom.xml
@@ -0,0 +1,33 @@
+
+ 4.0.0
+
+ spring-data-examples-utils
+ jar
+
+
+ org.springframework.data.examples
+ spring-data-examples
+ 2.0.0.BUILD-SNAPSHOT
+
+
+ Spring Data Examples Test Utilities
+ Utilities for Spring Data Example tests
+
+
+
+ org.springframework.data
+ spring-data-commons
+
+
+ junit
+ junit
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+
diff --git a/util/src/main/java/example/springdata/test/util/InfrastructureRule.java b/util/src/main/java/example/springdata/test/util/InfrastructureRule.java
new file mode 100644
index 000000000..be98f33e5
--- /dev/null
+++ b/util/src/main/java/example/springdata/test/util/InfrastructureRule.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018 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 example.springdata.test.util;
+
+import static java.lang.Boolean.*;
+
+import lombok.Value;
+
+import java.util.function.Supplier;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.rules.ExternalResource;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.data.util.Lazy;
+import org.springframework.util.Assert;
+
+/**
+ * A JUnit Rule for handling the discovery or creation of required infrastructure as for example databases. It supports
+ * multiple detection/creation strategies, supplied via {@link Supplier}. The system property
+ * {@code ignoreMissingInfrastructure} controls if tests get ignored when infrastructure is missing, or if the first
+ * test will throw an exception.
+ *
+ * This rule is intended to be instantiate by a Spring {@link org.springframework.context.ApplicationContext} and injected into tests.
+ * This enables shutdown of the infrastructure when the {@link org.springframework.context.ApplicationContext} gets shutdown.
+ *
+ * @author Jens Schauder
+ */
+public class InfrastructureRule extends ExternalResource implements DisposableBean {
+
+ private static final String IGNORE_MISSING_INFRASTRUCTURE_PROPERTY_NAME = "ignoreMissingInfrastructure";
+ private final boolean ignoreMissingInfrastructure;
+ private final Supplier> cachedResourceInfo;
+ private boolean isSubsequentCall = false;
+
+ /**
+ * Creates or find the infrastructure using the provided {@link Supplier}s. The suppliers get tried until the first one returns a valid {@link InfrastructureInfo}.
+ *
+ * @param possibleSources different strategies to acquire the required infrastructure.
+ */
+ public InfrastructureRule(Supplier>... possibleSources) {
+
+ this(getIgnoreMissingInfrastructureProperty(), possibleSources);
+ }
+
+ private static boolean getIgnoreMissingInfrastructureProperty() {
+ return Boolean.parseBoolean(System.getProperty(IGNORE_MISSING_INFRASTRUCTURE_PROPERTY_NAME, TRUE.toString()));
+ }
+
+ InfrastructureRule(boolean ignoreMissingInfrastructure, Supplier>... possibleSources) {
+
+ this(ignoreMissingInfrastructure, () -> buildInfoForFirstWorkingSource(possibleSources));
+
+ Assert.notEmpty(possibleSources, "We need at least one possibleSource for a resource.");
+ }
+
+ private InfrastructureRule(boolean ignoreMissingInfrastructure,
+ Supplier> resourceInfoSupplier) {
+
+ this.ignoreMissingInfrastructure = ignoreMissingInfrastructure;
+ this.cachedResourceInfo = Lazy.of(resourceInfoSupplier);
+ }
+
+ @Override
+ protected void before() throws Throwable {
+
+ if (!cachedResourceInfo.get().isValid()) {
+ String message = "Required infrastructure not available available";
+
+ if (ignoreMissingInfrastructure || isSubsequentCall) {
+ throw new AssumptionViolatedException(message);
+ } else {
+ isSubsequentCall = true;
+ throw new IllegalStateException(message);
+ }
+ }
+ }
+
+ public T getInfo() {
+ return cachedResourceInfo.get().getInfo();
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ cachedResourceInfo.get().destroy.run();
+ }
+
+ static private InfrastructureInfo buildInfoForFirstWorkingSource(
+ Supplier>... possibleSources) {
+
+ Throwable lastException = null;
+ for (Supplier> source : possibleSources) {
+
+ InfrastructureInfo info = source.get();
+ if (info.isValid()) {
+ return info;
+ }
+
+ lastException = info.getException();
+ }
+
+ return new InfrastructureInfo<>(false, null, lastException, () -> {});
+ }
+
+ /**
+ * Information about an attempt to find or create required infrastructure.
+ *
+ * @param the type of information offered to the tests about the infrastructure.
+ */
+ @Value
+ public static class InfrastructureInfo {
+
+ /** if the infrastructure was found or not */
+ boolean valid;
+
+ /** information about the infrastructure, e.g. an URL where to reach it. */
+ T info;
+
+ /** Any throwable thrown when failing to create or find the required infrastructure. */
+ Throwable exception;
+
+ /** A runnable to execute to shutdown the infrastructure. */
+ Runnable destroy;
+ }
+}
diff --git a/util/src/test/java/example/springdata/test/util/InfrastructureRuleUnitTests.java b/util/src/test/java/example/springdata/test/util/InfrastructureRuleUnitTests.java
new file mode 100644
index 000000000..b58e31e9e
--- /dev/null
+++ b/util/src/test/java/example/springdata/test/util/InfrastructureRuleUnitTests.java
@@ -0,0 +1,67 @@
+package example.springdata.test.util;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.Test;
+import org.junit.runners.model.Statement;
+
+/**
+ * Unit tests for the {@link InfrastructureRule}.
+ *
+ * @author Jens Schauder
+ */
+public class InfrastructureRuleUnitTests {
+
+ static final Statement EMPTY_STATEMENT = new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+
+ }
+ };
+
+ @Test
+ public void failingRuleIgnoresTestByDefault() {
+
+ InfrastructureRule