Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Apache Solr Module #2123

Merged
merged 34 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2eb27ed
Fixes #1562 Add Solr Module
raynigon Nov 30, 2019
fe7c481
Add incubation notice for solr module
raynigon Dec 1, 2019
7e4f7ba
Add example for solr container
raynigon Dec 1, 2019
928e1fc
Adjust Testcase to pass Java8 Compilation
raynigon Dec 1, 2019
4c5e901
Adjust Testcase to pass Java8 Compilation
raynigon Dec 1, 2019
29b1c41
Merge branch 'master' into master
raynigon Dec 1, 2019
9b1a5b7
Merge branch 'master' into master
raynigon Dec 2, 2019
b562865
Merge branch 'master' into master
raynigon Dec 6, 2019
24b6f37
Merge branch 'master' into master
raynigon Dec 8, 2019
ba6cca8
Merge branch 'master' into master
raynigon Dec 15, 2019
44bc3a8
Merge branch 'master' into master
raynigon Dec 18, 2019
46c16eb
Merge branch 'master' into master
raynigon Dec 19, 2019
b227579
Merge branch 'master' into master
raynigon Jan 13, 2020
a82ad46
Merge branch 'master' into master
raynigon Jan 21, 2020
d78c7ce
Merge branch 'master' into master
raynigon Jan 26, 2020
829411d
Merge branch 'master' into master
raynigon Jan 31, 2020
1df8fdd
Merge branch 'master' into master
raynigon Feb 12, 2020
2d8c55e
Merge branch 'master' into master
raynigon Feb 17, 2020
895f400
Merge branch 'master' into master
raynigon Mar 2, 2020
e67fee1
Merge branch 'master' into master
raynigon Mar 5, 2020
555248a
Merge branch 'master' into master
raynigon Mar 12, 2020
9fe83e8
Merge branch 'master' into master
raynigon Mar 13, 2020
2fd1407
Merge branch 'master' into master
raynigon Mar 20, 2020
2aaf8e1
Merge branch 'master' into master
raynigon Apr 1, 2020
7f1544b
Merge branch 'master' into master
raynigon Apr 5, 2020
ecbcb66
Merge branch 'master' into master
raynigon May 1, 2020
91b7757
Merge branch 'master' into master
rnorth May 8, 2020
681a5e9
Remove apache httpclient dependency
raynigon May 8, 2020
0bc11d0
Merge branch 'master' into master
raynigon May 8, 2020
001a76d
Use container IP Address
raynigon May 11, 2020
89c4c35
Use container IP Address
raynigon May 11, 2020
d67180a
improved code due to review
raynigon May 11, 2020
f8544d0
Merge branch 'master' into master
raynigon May 14, 2020
9f4ccc9
Merge branch 'master' into master
rnorth May 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/modules/solr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Solr Container

!!! note
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.


This module helps running [solr](https://lucene.apache.org/solr/) using Testcontainers.

Note that it's based on the [official Docker image](https://hub.docker.com/_/solr/).

## Usage example

You can start a solr container instance from any Java application by using:

<!--codeinclude-->
[Using a Solr container](../../modules/solr/src/test/java/org/testcontainers/containers/SolrContainerTest.java) inside_block:solrContainerUsage
<!--/codeinclude-->

## Adding this module to your project dependencies

Add the following dependency to your `pom.xml`/`build.gradle` file:

```groovy tab='Gradle'
testCompile "org.testcontainers:solr:{{latest_version}}"
```

```xml tab='Maven'
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>solr</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```
3 changes: 2 additions & 1 deletion examples/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ include 'redis-backed-cache'
include 'redis-backed-cache-testng'
include 'selenium-container'
include 'singleton-container'
include 'solr-container'
include 'spring-boot'
include 'cucumber'
include 'spring-boot-kotlin-redis'
include 'spock'
include 'spock'
18 changes: 18 additions & 0 deletions examples/solr-container/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
plugins {
id 'java'
}

repositories {
jcenter()
}

dependencies {
compileOnly "org.projectlombok:lombok:1.18.10"
annotationProcessor "org.projectlombok:lombok:1.18.10"

implementation 'org.apache.solr:solr-solrj:8.3.0'

testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:solr'

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example;

public interface SearchEngine {

public SearchResult search(String term);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example;

import java.util.List;
import java.util.Map;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SearchResult {

private long totalHits;

private List<Map<String, Object>> results;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example;

import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;

@RequiredArgsConstructor
public class SolrSearchEngine implements SearchEngine {

public static final String COLLECTION_NAME = "products";

private final SolrClient client;

@SneakyThrows
public SearchResult search(String term) {

SolrQuery query = new SolrQuery();
query.setQuery("title:" + ClientUtils.escapeQueryChars(term));
QueryResponse response = client.query(COLLECTION_NAME, query);
return createResult(response);
}

private SearchResult createResult(QueryResponse response) {
return SearchResult.builder()
.totalHits(response.getResults().getNumFound())
.results(response.getResults()
.stream()
.map(SolrDocument::getFieldValueMap)
.collect(Collectors.toList()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.example;

import static com.example.SolrSearchEngine.COLLECTION_NAME;
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.SolrInputField;
import org.junit.BeforeClass;
import org.junit.Test;
import org.testcontainers.containers.SolrContainer;

public class SolrQueryTest {

public static final SolrContainer solrContainer = new SolrContainer()
.withCollection(COLLECTION_NAME);

private static SolrClient solrClient;

@BeforeClass
public static void setUp() throws IOException, SolrServerException {
solrContainer.start();
solrClient = new Http2SolrClient.Builder("http://" + solrContainer.getContainerIpAddress() + ":" + solrContainer.getSolrPort() + "/solr").build();

// Add Sample Data
solrClient.add(COLLECTION_NAME, Collections.singletonList(
new SolrInputDocument(createMap(
"id", createInputField("id", "1"),
"title", createInputField("title", "old skool - trainers - shoes")
))
));

solrClient.add(COLLECTION_NAME, Collections.singletonList(
new SolrInputDocument(createMap(
"id", createInputField("id", "2"),
"title", createInputField("title", "print t-shirt")
))
));

solrClient.commit(COLLECTION_NAME);
}

@Test
public void testQueryForShoes() {
SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient);

SearchResult result = searchEngine.search("shoes");
assertEquals("When searching for shoes we expect one result", 1L, result.getTotalHits());
assertEquals("The result should have the id 1", "1", result.getResults().get(0).get("id"));
}

@Test
public void testQueryForTShirt() {
SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient);

SearchResult result = searchEngine.search("t-shirt");
assertEquals("When searching for t-shirt we expect one result", 1L, result.getTotalHits());
assertEquals("The result should have the id 2", "2", result.getResults().get(0).get("id"));
}

@Test
public void testQueryForAsterisk() {
SolrSearchEngine searchEngine = new SolrSearchEngine(solrClient);

SearchResult result = searchEngine.search("*");
assertEquals("When searching for * we expect no results", 0L, result.getTotalHits());
}

private static SolrInputField createInputField(String key, String value) {
SolrInputField inputField = new SolrInputField(key);
inputField.setValue(value);
return inputField;
}

private static Map<String, SolrInputField> createMap(String k0, SolrInputField v0, String k1, SolrInputField v1) {
Map<String, SolrInputField> result = new HashMap<>();
result.put(k0, v0);
result.put(k1, v1);
return result;
}
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ nav:
- modules/nginx.md
- modules/pulsar.md
- modules/rabbitmq.md
- modules/solr.md
- modules/toxiproxy.md
- modules/vault.md
- modules/webdriver_containers.md
Expand Down
7 changes: 7 additions & 0 deletions modules/solr/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
description = "Testcontainers :: Solr"

dependencies {
compile project(':testcontainers')
testCompile 'org.apache.solr:solr-solrj:8.3.0'

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package org.testcontainers.containers;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.io.IOUtils;

import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
* Utils class which can create collections and configurations.
*
* @author Simon Schneider
*/
public class SolrClientUtils {

private static OkHttpClient httpClient = new OkHttpClient();

/**
* Creates a new configuration and uploads the solrconfig.xml and schema.xml
*
* @param hostname the Hostname under which solr is reachable
* @param port the Port on which solr is running
* @param configurationName the name of the configuration which should be created
* @param solrConfig the url under which the solrconfig.xml can be found
* @param solrSchema the url under which the schema.xml can be found or null if the default schema should be used
*/
public static void uploadConfiguration(String hostname, int port, String configurationName, URL solrConfig, URL solrSchema) throws URISyntaxException, IOException {
Map<String, String> parameters = new HashMap<>();
parameters.put("action", "UPLOAD");
parameters.put("name", configurationName);
HttpUrl url = generateSolrURL(hostname, port, Arrays.asList("admin", "configs"), parameters);

byte[] configurationZipFile = generateConfigZipFile(solrConfig, solrSchema);
executePost(url, configurationZipFile);

}

/**
* Creates a new collection
*
* @param hostname the Hostname under which solr is reachable
* @param port The Port on which solr is running
* @param collectionName the name of the collection which should be created
* @param configurationName the name of the configuration which should used to create the collection
* or null if the default configuration should be used
*/
public static void createCollection(String hostname, int port, String collectionName, String configurationName) throws URISyntaxException, IOException {
Map<String, String> parameters = new HashMap<>();
parameters.put("action", "CREATE");
parameters.put("name", collectionName);
parameters.put("numShards", "1");
parameters.put("replicationFactor", "1");
parameters.put("wt", "json");
if (configurationName != null) {
parameters.put("collection.configName", configurationName);
}
HttpUrl url = generateSolrURL(hostname, port, Arrays.asList("admin", "collections"), parameters);
executePost(url, null);
}

private static void executePost(HttpUrl url, byte[] data) throws IOException {

RequestBody requestBody = data == null ?
RequestBody.create(MediaType.parse("text/plain"), "") :
RequestBody.create(MediaType.parse("application/octet-stream"), data);
;

Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = httpClient.newCall(request).execute();
raynigon marked this conversation as resolved.
Show resolved Hide resolved
if (!response.isSuccessful()) {
String responseBody = "";
if (response.body() != null) {
responseBody = response.body().string();
response.close();
}
throw new SolrClientUtilsException(response.code(), "Unable to upload binary\n" + responseBody);
}
if (response.body() != null) {
response.close();
}
}

private static HttpUrl generateSolrURL(String hostname, int port, List<String> pathSegments, Map<String, String> parameters) throws URISyntaxException {
HttpUrl.Builder builder = new HttpUrl.Builder();
builder.scheme("http");
builder.host(hostname);
builder.port(port);
// Path
builder.addPathSegment("solr");
if (pathSegments != null) {
pathSegments.forEach(builder::addPathSegment);
}
// Query Parameters
parameters.forEach(builder::addQueryParameter);
return builder.build();
}

private static byte[] generateConfigZipFile(URL solrConfiguration, URL solrSchema) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(bos);
// SolrConfig
zipOutputStream.putNextEntry(new ZipEntry("solrconfig.xml"));
IOUtils.copy(solrConfiguration.openStream(), zipOutputStream);
zipOutputStream.closeEntry();

// Solr Schema
if (solrSchema != null) {
zipOutputStream.putNextEntry(new ZipEntry("schema.xml"));
IOUtils.copy(solrSchema.openStream(), zipOutputStream);
zipOutputStream.closeEntry();
}

zipOutputStream.close();
return bos.toByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.testcontainers.containers;

public class SolrClientUtilsException extends RuntimeException {
public SolrClientUtilsException(int statusCode, String msg) {
super("Http Call Status: " + statusCode + "\n" + msg);
}
}
Loading