Skip to content

Commit

Permalink
Add Apache Solr Module (testcontainers#2123)
Browse files Browse the repository at this point in the history
  • Loading branch information
raynigon authored May 14, 2020
1 parent 1ed8d81 commit f84bcd6
Show file tree
Hide file tree
Showing 14 changed files with 584 additions and 1 deletion.
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();
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

0 comments on commit f84bcd6

Please sign in to comment.