From 08e326d98fd6bb94519fd50a349d7582817f2970 Mon Sep 17 00:00:00 2001 From: Oscar-Lilja-Simonsoft Date: Wed, 15 Jul 2026 10:21:08 +0200 Subject: [PATCH 1/4] Add multi-core Solr configuration model --- .../solr/runtime/SolrConnectionConfig.java | 17 +++++++++++++++++ .../solr/runtime/SolrDevserviceConfig.java | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java index 429327b..c32580e 100644 --- a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java @@ -5,6 +5,7 @@ import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; +import java.util.Map; import java.util.Optional; @ConfigMapping(prefix = "quarkus.solr") @@ -80,6 +81,22 @@ public interface SolrConnectionConfig { */ Optional defaultCollection(); + /** + * Named Solr clients for multi-core applications. + * + * @return the client configurations keyed by CDI name/core identifier + */ + Map clients(); + + interface ClientConfig { + /** + * The URL to the Solr core. + * + * @return + */ + String url(); + } + interface BasicAuthConfig { /** * Username to use for basic authentication. diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java index c76684e..b2c2ebd 100644 --- a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java @@ -5,6 +5,8 @@ import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; +import java.util.Map; + @ConfigMapping(prefix = "quarkus.solr.devservices") @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public interface SolrDevserviceConfig { @@ -30,4 +32,20 @@ public interface SolrDevserviceConfig { */ @WithDefault("9.6.1") String version(); + + /** + * Named Solr cores for multi-core dev services. + * + * @return the core configurations keyed by core name + */ + Map cores(); + + interface CoreConfig { + /** + * Classpath path to the Solr core config directory. + * + * @return + */ + String configPath(); + } } From b1fa9daaf3fab9275506b01cd07ec3380586c047 Mon Sep 17 00:00:00 2001 From: Oscar-Lilja-Simonsoft Date: Fri, 17 Jul 2026 08:18:00 +0200 Subject: [PATCH 2/4] Add Solr client registry for multi-core support --- .../solr/deployment/SolrProcessor.java | 2 + .../solr/runtime/SolrClientProducer.java | 27 +----- .../solr/runtime/SolrClientRegistry.java | 90 +++++++++++++++++++ 3 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java diff --git a/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java b/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java index e7f14e8..7f44c6e 100644 --- a/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java +++ b/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java @@ -1,6 +1,7 @@ package com.fntsoftware.solr.deployment; import com.fntsoftware.solr.runtime.SolrClientProducer; +import com.fntsoftware.solr.runtime.SolrClientRegistry; import com.fntsoftware.solr.runtime.SolrDevserviceConfig; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.IsNormal; @@ -27,6 +28,7 @@ class SolrProcessor { public AdditionalBeanBuildItem producer() { return AdditionalBeanBuildItem.builder() .addBeanClass(SolrClientProducer.class) + .addBeanClass(SolrClientRegistry.class) .setUnremovable() .build(); } diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientProducer.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientProducer.java index 63c5452..26744a5 100644 --- a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientProducer.java +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientProducer.java @@ -6,41 +6,18 @@ import jakarta.inject.Inject; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrServerException; -import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; -import org.eclipse.microprofile.context.ManagedExecutor; import java.io.IOException; -import java.util.concurrent.TimeUnit; @ApplicationScoped public class SolrClientProducer { @Inject - SolrConnectionConfig config; - - @Inject - ManagedExecutor executor; + SolrClientRegistry registry; @Produces @ApplicationScoped @LookupUnlessProperty(name = "quarkus.solr.enabled", stringValue = "false") public SolrClient getClient() throws SolrServerException, IOException { - HttpJdkSolrClient.Builder builder = new HttpJdkSolrClient.Builder(config.url()) - .withExecutor(executor) - .useHttp1_1(config.useHttp1_1()) - .withRequestTimeout(config.requestTimeout(), TimeUnit.MILLISECONDS) - .withConnectionTimeout(config.connectionTimeout(), TimeUnit.MILLISECONDS) - .withIdleTimeout(config.idleTimeout(), TimeUnit.MILLISECONDS) - .withFollowRedirects(config.followRedirects()); - - if (config.auth().isPresent()) { - builder.withBasicAuthCredentials(config.auth().get().username(), config.auth().get().password()); - } - if (config.defaultCollection().isPresent()) { - builder.withDefaultCollection(config.defaultCollection().get()); - } - - SolrClient c = builder.build(); - c.ping(); - return c; + return registry.defaultClient(); } } diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java new file mode 100644 index 0000000..aedbee2 --- /dev/null +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java @@ -0,0 +1,90 @@ +package com.fntsoftware.solr.runtime; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpJdkSolrClient; +import org.eclipse.microprofile.context.ManagedExecutor; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@ApplicationScoped +public class SolrClientRegistry { + private static final String DEFAULT_CLIENT = "default"; + + @Inject + SolrConnectionConfig config; + + @Inject + ManagedExecutor executor; + + private final Map clients = new HashMap<>(); + + public synchronized SolrClient defaultClient() throws SolrServerException, IOException { + return client(DEFAULT_CLIENT, config.url()); + } + + public synchronized SolrClient namedClient(String name) throws SolrServerException, IOException { + SolrConnectionConfig.ClientConfig clientConfig = config.clients().get(name); + if (clientConfig == null) { + throw new IllegalArgumentException("No Solr client configured with name '" + name + "'"); + } + return client(name, clientConfig.url()); + } + + private SolrClient client(String name, String url) throws SolrServerException, IOException { + SolrClient existing = clients.get(name); + if (existing != null) { + return existing; + } + SolrClient created = createClient(url); + clients.put(name, created); + return created; + } + + private SolrClient createClient(String url) throws SolrServerException, IOException { + HttpJdkSolrClient.Builder builder = new HttpJdkSolrClient.Builder(url) + .withExecutor(executor) + .useHttp1_1(config.useHttp1_1()) + .withRequestTimeout(config.requestTimeout(), TimeUnit.MILLISECONDS) + .withConnectionTimeout(config.connectionTimeout(), TimeUnit.MILLISECONDS) + .withIdleTimeout(config.idleTimeout(), TimeUnit.MILLISECONDS) + .withFollowRedirects(config.followRedirects()); + + if (config.auth().isPresent()) { + builder.withBasicAuthCredentials(config.auth().get().username(), config.auth().get().password()); + } + if (config.defaultCollection().isPresent()) { + builder.withDefaultCollection(config.defaultCollection().get()); + } + + SolrClient client = builder.build(); + client.ping(); + return client; + } + + @PreDestroy + void close() throws IOException { + IOException failure = null; + for (SolrClient client : clients.values()) { + try { + client.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + clients.clear(); + if (failure != null) { + throw failure; + } + } +} From edfc5b7e67d6df243792bf197a2ef463e688c4dd Mon Sep 17 00:00:00 2001 From: Oscar-Lilja-Simonsoft Date: Mon, 20 Jul 2026 16:04:10 +0200 Subject: [PATCH 3/4] Add named Solr clients for multi-core config Register synthetic CDI beans for configured Solr client names so consumers can inject core-specific clients with @Named("repositem") or @Named("reposxml"). The beans are backed by SolrClientRegistry and only created when quarkus.solr.enabled=true. Make the legacy single-core quarkus.solr.url and quarkus.solr.devservices.core config optional so multi-core-only configuration does not require default single-core settings. The default client and legacy single-core Dev Service still fail clearly if those values are used without being configured. Also add a QuarkusUnitTest that verifies named SolrClient beans resolve for repositem and reposxml. --- .../solr/deployment/SolrProcessor.java | 60 ++++++++++++++++++- .../solr/deployment/SolrNamedClientTest.java | 41 +++++++++++++ .../solr/runtime/NamedSolrClientCreator.java | 16 +++++ .../solr/runtime/SolrClientRegistry.java | 11 +++- .../solr/runtime/SolrConnectionConfig.java | 2 +- .../solr/runtime/SolrDevserviceConfig.java | 3 +- 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 deployment/src/test/java/com/fntsoftware/solr/deployment/SolrNamedClientTest.java create mode 100644 runtime/src/main/java/com/fntsoftware/solr/runtime/NamedSolrClientCreator.java diff --git a/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java b/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java index 7f44c6e..f2fc3b8 100644 --- a/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java +++ b/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java @@ -3,12 +3,18 @@ import com.fntsoftware.solr.runtime.SolrClientProducer; import com.fntsoftware.solr.runtime.SolrClientRegistry; import com.fntsoftware.solr.runtime.SolrDevserviceConfig; +import com.fntsoftware.solr.runtime.NamedSolrClientCreator; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; import io.quarkus.deployment.builditem.DevServicesResultBuildItem; import io.quarkus.runtime.LaunchMode; +import jakarta.inject.Singleton; +import org.apache.solr.client.solrj.SolrClient; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; @@ -16,6 +22,8 @@ import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; import java.io.IOException; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.function.BooleanSupplier; class SolrProcessor { @@ -23,6 +31,10 @@ class SolrProcessor { static volatile DevServicesResultBuildItem.RunningDevService devService; private static final String FEATURE = "solr"; + private static final String CLIENT_PREFIX = "quarkus.solr.clients."; + private static final String CLIENT_URL_SUFFIX = ".url"; + private static final String DEV_SERVICE_CORE_PREFIX = "quarkus.solr.devservices.cores."; + private static final String DEV_SERVICE_CORE_CONFIG_PATH_SUFFIX = ".config-path"; @BuildStep public AdditionalBeanBuildItem producer() { @@ -33,6 +45,22 @@ public AdditionalBeanBuildItem producer() { .build(); } + @BuildStep + public void namedSolrClients(BuildProducer syntheticBeans) { + if (!solrEnabled()) { + return; + } + for (String name : configuredNamedClients()) { + syntheticBeans.produce(SyntheticBeanBuildItem.configure(SolrClient.class) + .scope(Singleton.class) + .named(name) + .unremovable() + .creator(NamedSolrClientCreator.class) + .param(NamedSolrClientCreator.NAME_PARAM, name) + .done()); + } + } + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = WantsSolrDevService.class) public DevServicesResultBuildItem createContainer(CuratedApplicationShutdownBuildItem closeBuildItem) { if (devService != null) { @@ -49,15 +77,16 @@ public DevServicesResultBuildItem createContainer(CuratedApplicationShutdownBuil devService = null; }; closeBuildItem.addCloseTask(closeTask, false); + String core = singleCoreName(); ImageFromDockerfile image = new ImageFromDockerfile("quarkus/devservices/solr") .withFileFromClasspath(".", "solr").withDockerfileFromBuilder(builder -> { builder.from("solr:" + config.version()).withStatement( - new MultiArgsStatement("COPY --chown=solr:solr", ".", "/var/solr/data/" + config.core())); + new MultiArgsStatement("COPY --chown=solr:solr", ".", "/var/solr/data/" + core)); }); SolrContainer container = new SolrContainer(image); container.start(); Map props = Map.of("quarkus.solr.url", "http://" + container.getHost() + ":" - + container.getMappedPort(container.getPort()) + "/solr/" + config.core()); + + container.getMappedPort(container.getPort()) + "/solr/" + core); devService = new DevServicesResultBuildItem.RunningDevService(FEATURE, container.getContainerId(), container::close, props); return devService.toBuildItem(); @@ -92,4 +121,31 @@ protected void configure() { waitingFor(Wait.forLogMessage(".*Started Server.*", 1)); } } + + private static boolean solrEnabled() { + return ConfigProvider.getConfig().getOptionalValue("quarkus.solr.enabled", Boolean.class).orElse(false); + } + + private static Set configuredNamedClients() { + Config config = ConfigProvider.getConfig(); + Set names = new TreeSet<>(); + for (String propertyName : config.getPropertyNames()) { + clientName(propertyName, CLIENT_PREFIX, CLIENT_URL_SUFFIX).ifPresent(names::add); + clientName(propertyName, DEV_SERVICE_CORE_PREFIX, DEV_SERVICE_CORE_CONFIG_PATH_SUFFIX).ifPresent(names::add); + } + return names; + } + + private static java.util.Optional clientName(String propertyName, String prefix, String suffix) { + if (!propertyName.startsWith(prefix) || !propertyName.endsWith(suffix)) { + return java.util.Optional.empty(); + } + String name = propertyName.substring(prefix.length(), propertyName.length() - suffix.length()); + return name.isBlank() ? java.util.Optional.empty() : java.util.Optional.of(name); + } + + private String singleCoreName() { + return config.core().orElseThrow( + () -> new IllegalStateException("quarkus.solr.devservices.core is required for single-core Solr Dev Service")); + } } diff --git a/deployment/src/test/java/com/fntsoftware/solr/deployment/SolrNamedClientTest.java b/deployment/src/test/java/com/fntsoftware/solr/deployment/SolrNamedClientTest.java new file mode 100644 index 0000000..b7cd628 --- /dev/null +++ b/deployment/src/test/java/com/fntsoftware/solr/deployment/SolrNamedClientTest.java @@ -0,0 +1,41 @@ +package com.fntsoftware.solr.deployment; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.test.QuarkusUnitTest; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.apache.solr.client.solrj.SolrClient; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SolrNamedClientTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset(""" + quarkus.solr.enabled=true + quarkus.solr.devservices.enabled=false + quarkus.solr.clients.repositem.url=http://localhost:1/solr/repositem + quarkus.solr.clients.reposxml.url=http://localhost:1/solr/reposxml + """), "application.properties")); + + @Inject + @Named("repositem") + Instance repositem; + + @Inject + @Named("reposxml") + Instance reposxml; + + @Test + void shouldResolveNamedSolrClients() { + assertTrue(repositem.isResolvable()); + assertTrue(reposxml.isResolvable()); + } +} diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/NamedSolrClientCreator.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/NamedSolrClientCreator.java new file mode 100644 index 0000000..01120a2 --- /dev/null +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/NamedSolrClientCreator.java @@ -0,0 +1,16 @@ +package com.fntsoftware.solr.runtime; + +import io.quarkus.arc.BeanCreator; +import io.quarkus.arc.SyntheticCreationalContext; +import org.apache.solr.client.solrj.SolrClient; + +public class NamedSolrClientCreator implements BeanCreator { + + public static final String NAME_PARAM = "name"; + + @Override + public SolrClient create(SyntheticCreationalContext context) { + String name = (String) context.getParams().get(NAME_PARAM); + return context.getInjectedReference(SolrClientRegistry.class).namedClientForBean(name); + } +} diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java index aedbee2..11c859d 100644 --- a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrClientRegistry.java @@ -26,7 +26,8 @@ public class SolrClientRegistry { private final Map clients = new HashMap<>(); public synchronized SolrClient defaultClient() throws SolrServerException, IOException { - return client(DEFAULT_CLIENT, config.url()); + return client(DEFAULT_CLIENT, config.url().orElseThrow( + () -> new IllegalStateException("quarkus.solr.url is required for the default Solr client"))); } public synchronized SolrClient namedClient(String name) throws SolrServerException, IOException { @@ -37,6 +38,14 @@ public synchronized SolrClient namedClient(String name) throws SolrServerExcepti return client(name, clientConfig.url()); } + public SolrClient namedClientForBean(String name) { + try { + return namedClient(name); + } catch (SolrServerException | IOException e) { + throw new IllegalStateException("Failed to create Solr client '" + name + "'", e); + } + } + private SolrClient client(String name, String url) throws SolrServerException, IOException { SolrClient existing = clients.get(name); if (existing != null) { diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java index c32580e..daa430d 100644 --- a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrConnectionConfig.java @@ -25,7 +25,7 @@ public interface SolrConnectionConfig { * * @return */ - String url(); + Optional url(); /** * Whether the SolrJ client should follow redirects or not diff --git a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java index b2c2ebd..aa6d379 100644 --- a/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java +++ b/runtime/src/main/java/com/fntsoftware/solr/runtime/SolrDevserviceConfig.java @@ -6,6 +6,7 @@ import io.smallrye.config.WithDefault; import java.util.Map; +import java.util.Optional; @ConfigMapping(prefix = "quarkus.solr.devservices") @ConfigRoot(phase = ConfigPhase.BUILD_TIME) @@ -23,7 +24,7 @@ public interface SolrDevserviceConfig { * * @return */ - String core(); + Optional core(); /** * Which Solr version to use From a2f2e7459516a474de8ae61605d54550122f15bd Mon Sep 17 00:00:00 2001 From: Oscar-Lilja-Simonsoft Date: Tue, 21 Jul 2026 12:46:44 +0200 Subject: [PATCH 4/4] Add multi-core Solr Dev Service support Allow Solr Dev Service to create multiple cores in a single Solr container from quarkus.solr.devservices.cores..config-path. Each core is copied into /var/solr/data/, waited on through /solr//admin/ping, and exposed to runtime as quarkus.solr.clients..url. Keep the legacy single-core Dev Service path intact for quarkus.solr.devservices.core and quarkus.solr.url. Add a QuarkusUnitTest that starts repositem and reposxml in the same Dev Service container and verifies both named SolrClient injections by pinging both cores. Document the single-core and multi-core configuration in README. Also align Testcontainers with docker-java 3.7.1 because Quarkus 3.12 otherwise resolves an older Docker client that fails with current Docker Desktop. --- README.md | 45 +++++++++--- deployment/pom.xml | 13 +++- .../solr/deployment/SolrProcessor.java | 70 +++++++++++++++--- .../SolrMultiCoreDevServiceTest.java | 72 +++++++++++++++++++ 4 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 deployment/src/test/java/com/fntsoftware/solr/deployment/SolrMultiCoreDevServiceTest.java diff --git a/README.md b/README.md index 0ec0597..ada71d4 100644 --- a/README.md +++ b/README.md @@ -6,29 +6,48 @@ Solr container. ## Configuration -The devservice is enabled by default in development mode. However, the name of the core the service should create in the -Solr instance needs to be set and the Solr version can be configured in your `application.properties`: +The extension is opt-in. Enable it in `application.properties` when the application should inject Solr clients: + +```properties +quarkus.solr.enabled=true +``` + +For a single-core Dev Service, set the core name. The extension expects the Solr core config in the `solr` directory on +the application classpath: ```properties -# enabled by default quarkus.solr.devservices.enabled=true -# required quarkus.solr.devservices.core= -# default version is the latest stable one quarkus.solr.devservices.version=9.6.1 ``` -Also, the extension expects the configuration for the Solr core in the `solr` directory in the resources of your -project. +For multi-core Dev Services, configure one classpath config directory per core: + +```properties +quarkus.solr.devservices.cores.repositem.config-path=se/repos/indexing/solr/repositem +quarkus.solr.devservices.cores.reposxml.config-path=se/simonsoft/cms/indexing/xml/solr/reposxml +``` + +Each configured core can then be injected as a named client: + +```java +@Inject +@Named("repositem") +SolrClient repositem; + +@Inject +@Named("reposxml") +SolrClient reposxml; +``` ### Prod For production usage the configurations above do not matter. Only the URL of the Solr instance to connect to needs to be -provided. Optionally the SolrClient can be disabled (it's enabled by default): +provided: ```properties +quarkus.solr.enabled=true quarkus.solr.url=https://mydomain.fun/solr/mycore -quarkus.solr.enabled=false quarkus.solr.request-timeout=0 quarkus.solr.idle-timeout=60000 quarkus.solr.connection-timeout=60000 @@ -42,6 +61,14 @@ quarkus.solr.use-http1-1=false > In development mode this URL configuration is done automatically and filled with the host, port and core name of the > devservice container +For multiple external cores, configure named clients directly: + +```properties +quarkus.solr.enabled=true +quarkus.solr.clients.repositem.url=https://mydomain.fun/solr/repositem +quarkus.solr.clients.reposxml.url=https://mydomain.fun/solr/reposxml +``` + ## Usage The extension comes with a provider for a `SolrClient` bean, which is connected to the configured Solr diff --git a/deployment/pom.xml b/deployment/pom.xml index d50a4b6..219c238 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -27,7 +27,18 @@ org.testcontainers testcontainers - 1.20.0 + 2.0.4 + + + + com.github.docker-java + docker-java-api + 3.7.1 + + + com.github.docker-java + docker-java-transport-zerodep + 3.7.1 io.quarkus diff --git a/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java b/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java index f2fc3b8..3769b54 100644 --- a/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java +++ b/deployment/src/main/java/com/fntsoftware/solr/deployment/SolrProcessor.java @@ -16,14 +16,20 @@ import org.apache.solr.client.solrj.SolrClient; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.containers.wait.strategy.WaitStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; import org.testcontainers.images.builder.dockerfile.statement.MultiArgsStatement; import java.io.IOException; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.TreeMap; import java.util.function.BooleanSupplier; class SolrProcessor { @@ -35,6 +41,8 @@ class SolrProcessor { private static final String CLIENT_URL_SUFFIX = ".url"; private static final String DEV_SERVICE_CORE_PREFIX = "quarkus.solr.devservices.cores."; private static final String DEV_SERVICE_CORE_CONFIG_PATH_SUFFIX = ".config-path"; + private static final Type SOLR_CLIENT_REGISTRY_TYPE = + Type.create(DotName.createSimple(SolrClientRegistry.class.getName()), Type.Kind.CLASS); @BuildStep public AdditionalBeanBuildItem producer() { @@ -55,6 +63,7 @@ public void namedSolrClients(BuildProducer syntheticBean .scope(Singleton.class) .named(name) .unremovable() + .addInjectionPoint(SOLR_CLIENT_REGISTRY_TYPE) .creator(NamedSolrClientCreator.class) .param(NamedSolrClientCreator.NAME_PARAM, name) .done()); @@ -77,13 +86,23 @@ public DevServicesResultBuildItem createContainer(CuratedApplicationShutdownBuil devService = null; }; closeBuildItem.addCloseTask(closeTask, false); - String core = singleCoreName(); + + Map cores = new TreeMap<>(config.cores()); + if (!cores.isEmpty()) { + return createMultiCoreContainer(cores); + } + return createSingleCoreContainer(); + } + + private DevServicesResultBuildItem createSingleCoreContainer() { + String core = config.core().orElseThrow( + () -> new IllegalStateException("quarkus.solr.devservices.core is required for single-core Solr Dev Service")); ImageFromDockerfile image = new ImageFromDockerfile("quarkus/devservices/solr") .withFileFromClasspath(".", "solr").withDockerfileFromBuilder(builder -> { builder.from("solr:" + config.version()).withStatement( new MultiArgsStatement("COPY --chown=solr:solr", ".", "/var/solr/data/" + core)); }); - SolrContainer container = new SolrContainer(image); + SolrContainer container = new SolrContainer(image, Set.of(core)); container.start(); Map props = Map.of("quarkus.solr.url", "http://" + container.getHost() + ":" + container.getMappedPort(container.getPort()) + "/solr/" + core); @@ -92,6 +111,30 @@ public DevServicesResultBuildItem createContainer(CuratedApplicationShutdownBuil return devService.toBuildItem(); } + private DevServicesResultBuildItem createMultiCoreContainer(Map cores) { + ImageFromDockerfile image = new ImageFromDockerfile("quarkus/devservices/solr"); + for (Map.Entry core : cores.entrySet()) { + image.withFileFromClasspath(core.getKey(), core.getValue().configPath()); + } + image.withDockerfileFromBuilder(builder -> { + builder.from("solr:" + config.version()); + for (String core : cores.keySet()) { + builder.withStatement(new MultiArgsStatement( + "COPY --chown=solr:solr", core, "/var/solr/data/" + core)); + } + }); + + SolrContainer container = new SolrContainer(image, cores.keySet()); + container.start(); + Map props = new LinkedHashMap<>(); + for (String core : cores.keySet()) { + props.put(CLIENT_PREFIX + core + CLIENT_URL_SUFFIX, solrCoreUrl(container, core)); + } + devService = new DevServicesResultBuildItem.RunningDevService(FEATURE, container.getContainerId(), + container::close, props); + return devService.toBuildItem(); + } + static class WantsSolrDevService implements BooleanSupplier { LaunchMode launchMode; SolrDevserviceConfig config; @@ -99,15 +142,17 @@ static class WantsSolrDevService implements BooleanSupplier { public boolean getAsBoolean() { Boolean devServicesActive = ConfigProvider.getConfig().getValue("quarkus.devservices.enabled", Boolean.class); - return launchMode.isDevOrTest() && devServicesActive && config.enabled(); + return launchMode.isDevOrTest() && solrEnabled() && devServicesActive && config.enabled(); } } private static class SolrContainer extends GenericContainer { static final int PORT = 8983; + private final Set cores; - public SolrContainer(ImageFromDockerfile image) { + public SolrContainer(ImageFromDockerfile image, Set cores) { super(image); + this.cores = Set.copyOf(cores); } public int getPort() { @@ -118,10 +163,20 @@ public int getPort() { protected void configure() { super.configure(); addExposedPort(PORT); - waitingFor(Wait.forLogMessage(".*Started Server.*", 1)); + waitingFor(waitForCores(cores)); } } + private static WaitStrategy waitForCores(Set cores) { + WaitAllStrategy wait = new WaitAllStrategy(); + for (String core : cores) { + wait.withStrategy(Wait.forHttp("/solr/" + core + "/admin/ping") + .forPort(SolrContainer.PORT) + .forStatusCode(200)); + } + return wait; + } + private static boolean solrEnabled() { return ConfigProvider.getConfig().getOptionalValue("quarkus.solr.enabled", Boolean.class).orElse(false); } @@ -144,8 +199,7 @@ private static java.util.Optional clientName(String propertyName, String return name.isBlank() ? java.util.Optional.empty() : java.util.Optional.of(name); } - private String singleCoreName() { - return config.core().orElseThrow( - () -> new IllegalStateException("quarkus.solr.devservices.core is required for single-core Solr Dev Service")); + private static String solrCoreUrl(SolrContainer container, String core) { + return "http://" + container.getHost() + ":" + container.getMappedPort(container.getPort()) + "/solr/" + core; } } diff --git a/deployment/src/test/java/com/fntsoftware/solr/deployment/SolrMultiCoreDevServiceTest.java b/deployment/src/test/java/com/fntsoftware/solr/deployment/SolrMultiCoreDevServiceTest.java new file mode 100644 index 0000000..4f581eb --- /dev/null +++ b/deployment/src/test/java/com/fntsoftware/solr/deployment/SolrMultiCoreDevServiceTest.java @@ -0,0 +1,72 @@ +package com.fntsoftware.solr.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.quarkus.test.QuarkusUnitTest; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.apache.solr.client.solrj.SolrClient; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SolrMultiCoreDevServiceTest { + + private static final String SOLRCONFIG = """ + + 9.6.1 + + + + + + + *:* + + + + """; + + private static final String SCHEMA = """ + + + + id + + """; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource(new StringAsset(""" + quarkus.solr.enabled=true + quarkus.solr.devservices.cores.repositem.config-path=solr/repositem + quarkus.solr.devservices.cores.reposxml.config-path=solr/reposxml + """), "application.properties") + .addAsResource(new StringAsset(""" + name=repositem + """), "solr/repositem/core.properties") + .addAsResource(new StringAsset(SOLRCONFIG), "solr/repositem/conf/solrconfig.xml") + .addAsResource(new StringAsset(SCHEMA), "solr/repositem/conf/schema.xml") + .addAsResource(new StringAsset(""" + name=reposxml + """), "solr/reposxml/core.properties") + .addAsResource(new StringAsset(SOLRCONFIG), "solr/reposxml/conf/solrconfig.xml") + .addAsResource(new StringAsset(SCHEMA), "solr/reposxml/conf/schema.xml")); + + @Inject + @Named("repositem") + SolrClient repositem; + + @Inject + @Named("reposxml") + SolrClient reposxml; + + @Test + void shouldStartSolrWithMultipleCores() throws Exception { + assertEquals(0, repositem.ping().getStatus()); + assertEquals(0, reposxml.ping().getStatus()); + } +}