Skip to content

Commit f84bcd6

Browse files
authored
Add Apache Solr Module (#2123)
1 parent 1ed8d81 commit f84bcd6

File tree

14 files changed

+584
-1
lines changed

14 files changed

+584
-1
lines changed

docs/modules/solr.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Solr Container
2+
3+
!!! note
4+
This module is INCUBATING. While it is ready for use and operational in the current version of Testcontainers, it is possible that it may receive breaking changes in the future. See [our contributing guidelines](/contributing/#incubating-modules) for more information on our incubating modules policy.
5+
6+
7+
This module helps running [solr](https://lucene.apache.org/solr/) using Testcontainers.
8+
9+
Note that it's based on the [official Docker image](https://hub.docker.com/_/solr/).
10+
11+
## Usage example
12+
13+
You can start a solr container instance from any Java application by using:
14+
15+
<!--codeinclude-->
16+
[Using a Solr container](../../modules/solr/src/test/java/org/testcontainers/containers/SolrContainerTest.java) inside_block:solrContainerUsage
17+
<!--/codeinclude-->
18+
19+
## Adding this module to your project dependencies
20+
21+
Add the following dependency to your `pom.xml`/`build.gradle` file:
22+
23+
```groovy tab='Gradle'
24+
testCompile "org.testcontainers:solr:{{latest_version}}"
25+
```
26+
27+
```xml tab='Maven'
28+
<dependency>
29+
<groupId>org.testcontainers</groupId>
30+
<artifactId>solr</artifactId>
31+
<version>{{latest_version}}</version>
32+
<scope>test</scope>
33+
</dependency>
34+
```

examples/settings.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ include 'redis-backed-cache'
1010
include 'redis-backed-cache-testng'
1111
include 'selenium-container'
1212
include 'singleton-container'
13+
include 'solr-container'
1314
include 'spring-boot'
1415
include 'cucumber'
1516
include 'spring-boot-kotlin-redis'
16-
include 'spock'
17+
include 'spock'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
repositories {
6+
jcenter()
7+
}
8+
9+
dependencies {
10+
compileOnly "org.projectlombok:lombok:1.18.10"
11+
annotationProcessor "org.projectlombok:lombok:1.18.10"
12+
13+
implementation 'org.apache.solr:solr-solrj:8.3.0'
14+
15+
testImplementation 'org.testcontainers:testcontainers'
16+
testImplementation 'org.testcontainers:solr'
17+
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example;
2+
3+
public interface SearchEngine {
4+
5+
public SearchResult search(String term);
6+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.example;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
import lombok.AllArgsConstructor;
7+
import lombok.Builder;
8+
import lombok.Data;
9+
import lombok.NoArgsConstructor;
10+
11+
@Data
12+
@Builder
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
public class SearchResult {
16+
17+
private long totalHits;
18+
19+
private List<Map<String, Object>> results;
20+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.example;
2+
3+
import java.util.stream.Collectors;
4+
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.SneakyThrows;
7+
8+
import org.apache.solr.client.solrj.SolrClient;
9+
import org.apache.solr.client.solrj.SolrQuery;
10+
import org.apache.solr.client.solrj.response.QueryResponse;
11+
import org.apache.solr.client.solrj.util.ClientUtils;
12+
import org.apache.solr.common.SolrDocument;
13+
14+
@RequiredArgsConstructor
15+
public class SolrSearchEngine implements SearchEngine {
16+
17+
public static final String COLLECTION_NAME = "products";
18+
19+
private final SolrClient client;
20+
21+
@SneakyThrows
22+
public SearchResult search(String term) {
23+
24+
SolrQuery query = new SolrQuery();
25+
query.setQuery("title:" + ClientUtils.escapeQueryChars(term));
26+
QueryResponse response = client.query(COLLECTION_NAME, query);
27+
return createResult(response);
28+
}
29+
30+
private SearchResult createResult(QueryResponse response) {
31+
return SearchResult.builder()
32+
.totalHits(response.getResults().getNumFound())
33+
.results(response.getResults()
34+
.stream()
35+
.map(SolrDocument::getFieldValueMap)
36+
.collect(Collectors.toList()))
37+
.build();
38+
}
39+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.example;
2+
3+
import static com.example.SolrSearchEngine.COLLECTION_NAME;
4+
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
5+
6+
import java.io.IOException;
7+
import java.util.Collections;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
11+
import org.apache.solr.client.solrj.SolrClient;
12+
import org.apache.solr.client.solrj.SolrServerException;
13+
import org.apache.solr.client.solrj.impl.Http2SolrClient;
14+
import org.apache.solr.common.SolrInputDocument;
15+
import org.apache.solr.common.SolrInputField;
16+
import org.junit.BeforeClass;
17+
import org.junit.Test;
18+
import org.testcontainers.containers.SolrContainer;
19+
20+
public class SolrQueryTest {
21+
22+
public static final SolrContainer solrContainer = new SolrContainer()
23+
.withCollection(COLLECTION_NAME);
24+
25+
private static SolrClient solrClient;
26+
27+
@BeforeClass
28+
public static void setUp() throws IOException, SolrServerException {
29+
solrContainer.start();
30+
solrClient = new Http2SolrClient.Builder("http://" + solrContainer.getContainerIpAddress() + ":" + solrContainer.getSolrPort() + "/solr").build();
31+
32+
// Add Sample Data
33+
solrClient.add(COLLECTION_NAME, Collections.singletonList(
34+
new SolrInputDocument(createMap(
35+
"id", createInputField("id", "1"),
36+
"title", createInputField("title", "old skool - trainers - shoes")
37+
))
38+
));
39+
40+
solrClient.add(COLLECTION_NAME, Collections.singletonList(
41+
new SolrInputDocument(createMap(
42+
"id", createInputField("id", "2"),
43+
"title", createInputField("title", "print t-shirt")
44+
))
45+
));
46+
47+
solrClient.commit(COLLECTION_NAME);
48+
}
49+
50+
@Test
51+
public void testQueryForShoes() {
52+
SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient);
53+
54+
SearchResult result = searchEngine.search("shoes");
55+
assertEquals("When searching for shoes we expect one result", 1L, result.getTotalHits());
56+
assertEquals("The result should have the id 1", "1", result.getResults().get(0).get("id"));
57+
}
58+
59+
@Test
60+
public void testQueryForTShirt() {
61+
SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient);
62+
63+
SearchResult result = searchEngine.search("t-shirt");
64+
assertEquals("When searching for t-shirt we expect one result", 1L, result.getTotalHits());
65+
assertEquals("The result should have the id 2", "2", result.getResults().get(0).get("id"));
66+
}
67+
68+
@Test
69+
public void testQueryForAsterisk() {
70+
SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient);
71+
72+
SearchResult result = searchEngine.search("*");
73+
assertEquals("When searching for * we expect no results", 0L, result.getTotalHits());
74+
}
75+
76+
private static SolrInputField createInputField(String key, String value) {
77+
SolrInputField inputField = new SolrInputField(key);
78+
inputField.setValue(value);
79+
return inputField;
80+
}
81+
82+
private static Map<String, SolrInputField> createMap(String k0, SolrInputField v0, String k1, SolrInputField v1) {
83+
Map<String, SolrInputField> result = new HashMap<>();
84+
result.put(k0, v0);
85+
result.put(k1, v1);
86+
return result;
87+
}
88+
}

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ nav:
6666
- modules/nginx.md
6767
- modules/pulsar.md
6868
- modules/rabbitmq.md
69+
- modules/solr.md
6970
- modules/toxiproxy.md
7071
- modules/vault.md
7172
- modules/webdriver_containers.md

modules/solr/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
description = "Testcontainers :: Solr"
2+
3+
dependencies {
4+
compile project(':testcontainers')
5+
testCompile 'org.apache.solr:solr-solrj:8.3.0'
6+
7+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package org.testcontainers.containers;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
import java.net.URISyntaxException;
6+
import java.net.URL;
7+
import java.util.Arrays;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.zip.ZipEntry;
12+
import java.util.zip.ZipOutputStream;
13+
14+
import org.apache.commons.io.IOUtils;
15+
16+
import okhttp3.HttpUrl;
17+
import okhttp3.MediaType;
18+
import okhttp3.OkHttpClient;
19+
import okhttp3.Request;
20+
import okhttp3.RequestBody;
21+
import okhttp3.Response;
22+
23+
/**
24+
* Utils class which can create collections and configurations.
25+
*
26+
* @author Simon Schneider
27+
*/
28+
public class SolrClientUtils {
29+
30+
private static OkHttpClient httpClient = new OkHttpClient();
31+
32+
/**
33+
* Creates a new configuration and uploads the solrconfig.xml and schema.xml
34+
*
35+
* @param hostname the Hostname under which solr is reachable
36+
* @param port the Port on which solr is running
37+
* @param configurationName the name of the configuration which should be created
38+
* @param solrConfig the url under which the solrconfig.xml can be found
39+
* @param solrSchema the url under which the schema.xml can be found or null if the default schema should be used
40+
*/
41+
public static void uploadConfiguration(String hostname, int port, String configurationName, URL solrConfig, URL solrSchema) throws URISyntaxException, IOException {
42+
Map<String, String> parameters = new HashMap<>();
43+
parameters.put("action", "UPLOAD");
44+
parameters.put("name", configurationName);
45+
HttpUrl url = generateSolrURL(hostname, port, Arrays.asList("admin", "configs"), parameters);
46+
47+
byte[] configurationZipFile = generateConfigZipFile(solrConfig, solrSchema);
48+
executePost(url, configurationZipFile);
49+
50+
}
51+
52+
/**
53+
* Creates a new collection
54+
*
55+
* @param hostname the Hostname under which solr is reachable
56+
* @param port The Port on which solr is running
57+
* @param collectionName the name of the collection which should be created
58+
* @param configurationName the name of the configuration which should used to create the collection
59+
* or null if the default configuration should be used
60+
*/
61+
public static void createCollection(String hostname, int port, String collectionName, String configurationName) throws URISyntaxException, IOException {
62+
Map<String, String> parameters = new HashMap<>();
63+
parameters.put("action", "CREATE");
64+
parameters.put("name", collectionName);
65+
parameters.put("numShards", "1");
66+
parameters.put("replicationFactor", "1");
67+
parameters.put("wt", "json");
68+
if (configurationName != null) {
69+
parameters.put("collection.configName", configurationName);
70+
}
71+
HttpUrl url = generateSolrURL(hostname, port, Arrays.asList("admin", "collections"), parameters);
72+
executePost(url, null);
73+
}
74+
75+
private static void executePost(HttpUrl url, byte[] data) throws IOException {
76+
77+
RequestBody requestBody = data == null ?
78+
RequestBody.create(MediaType.parse("text/plain"), "") :
79+
RequestBody.create(MediaType.parse("application/octet-stream"), data);
80+
;
81+
82+
Request request = new Request.Builder()
83+
.url(url)
84+
.post(requestBody)
85+
.build();
86+
Response response = httpClient.newCall(request).execute();
87+
if (!response.isSuccessful()) {
88+
String responseBody = "";
89+
if (response.body() != null) {
90+
responseBody = response.body().string();
91+
response.close();
92+
}
93+
throw new SolrClientUtilsException(response.code(), "Unable to upload binary\n" + responseBody);
94+
}
95+
if (response.body() != null) {
96+
response.close();
97+
}
98+
}
99+
100+
private static HttpUrl generateSolrURL(String hostname, int port, List<String> pathSegments, Map<String, String> parameters) throws URISyntaxException {
101+
HttpUrl.Builder builder = new HttpUrl.Builder();
102+
builder.scheme("http");
103+
builder.host(hostname);
104+
builder.port(port);
105+
// Path
106+
builder.addPathSegment("solr");
107+
if (pathSegments != null) {
108+
pathSegments.forEach(builder::addPathSegment);
109+
}
110+
// Query Parameters
111+
parameters.forEach(builder::addQueryParameter);
112+
return builder.build();
113+
}
114+
115+
private static byte[] generateConfigZipFile(URL solrConfiguration, URL solrSchema) throws IOException {
116+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
117+
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
118+
// SolrConfig
119+
zipOutputStream.putNextEntry(new ZipEntry("solrconfig.xml"));
120+
IOUtils.copy(solrConfiguration.openStream(), zipOutputStream);
121+
zipOutputStream.closeEntry();
122+
123+
// Solr Schema
124+
if (solrSchema != null) {
125+
zipOutputStream.putNextEntry(new ZipEntry("schema.xml"));
126+
IOUtils.copy(solrSchema.openStream(), zipOutputStream);
127+
zipOutputStream.closeEntry();
128+
}
129+
130+
zipOutputStream.close();
131+
return bos.toByteArray();
132+
}
133+
}

0 commit comments

Comments
 (0)