This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new 325d821ed2c Separate core specific Request Writers from node specific
"built in" ones. Move core specific to using ImplicitPlugins.json. (#4073)
325d821ed2c is described below
commit 325d821ed2c4c13d6137daaa540ea139c5c3fb53
Author: Eric Pugh <[email protected]>
AuthorDate: Sat Jan 31 10:05:26 2026 -0500
Separate core specific Request Writers from node specific "built in" ones.
Move core specific to using ImplicitPlugins.json. (#4073)
This PR moves core-specific QueryResponseWriter defaults out of hardcoded
Java and into ImplicitPlugins.json config file, while introducing a minimal
built-in response writer set for admin/container-level requests that have no
SolrCore via ResponseWritersRegistery class. We also also introduce a
FileStreamResponseWriter that replaces a anonymous class that required special
logic to create.
---
.../admin-response-writers-minimal-set.yml | 9 ++
.../solr/bench/search/QueryResponseWriters.java | 2 -
.../src/java/org/apache/solr/core/SolrCore.java | 146 ++++++++++----------
.../solr/handler/admin/SystemInfoHandler.java | 7 +-
.../org/apache/solr/request/SolrQueryRequest.java | 16 ++-
.../solr/response/FileStreamResponseWriter.java | 67 +++++++++
.../solr/response/ResponseWritersRegistry.java | 93 +++++++++++++
.../apache/solr/response/SolrQueryResponse.java | 4 +-
.../java/org/apache/solr/servlet/HttpSolrCall.java | 4 +-
solr/core/src/resources/ImplicitPlugins.json | 20 +++
.../org/apache/solr/core/TestImplicitPlugins.java | 78 +++++++++++
.../response/TestFileStreamResponseWriter.java | 149 +++++++++++++++++++++
.../solr/response/TestResponseWritersRegistry.java | 64 +++++++++
13 files changed, 570 insertions(+), 89 deletions(-)
diff --git a/changelog/unreleased/admin-response-writers-minimal-set.yml
b/changelog/unreleased/admin-response-writers-minimal-set.yml
new file mode 100644
index 00000000000..64b59f93ab6
--- /dev/null
+++ b/changelog/unreleased/admin-response-writers-minimal-set.yml
@@ -0,0 +1,9 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Introduce minimal set of request writers for node/container-level
requests. Core-specific request writers now leverage ImplicitPlugins.json for
creation.
+type: other
+authors:
+ - name: Eric Pugh
+ - name: David Smiley
+links:
+- name: PR#4073
+ url: https://github.com/apache/solr/pull/4073
diff --git
a/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
b/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
index 67d931c9217..4b6118fed27 100644
---
a/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
+++
b/solr/benchmark/src/java/org/apache/solr/bench/search/QueryResponseWriters.java
@@ -30,7 +30,6 @@ import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.response.InputStreamResponseParser;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.core.SolrCore;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
@@ -58,7 +57,6 @@ public class QueryResponseWriters {
@State(Scope.Benchmark)
public static class BenchState {
- /** See {@link SolrCore#DEFAULT_RESPONSE_WRITERS} */
@Param({CommonParams.JAVABIN, CommonParams.JSON, "cbor", "smile", "xml",
"raw"})
String wt;
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java
b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index ee97a2759b2..fc50c62cb1f 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -17,8 +17,6 @@
package org.apache.solr.core;
import static org.apache.solr.common.params.CommonParams.PATH;
-import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT;
-import static
org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT;
import static org.apache.solr.metrics.SolrCoreMetricManager.COLLECTION_ATTR;
import static org.apache.solr.metrics.SolrCoreMetricManager.CORE_ATTR;
import static org.apache.solr.metrics.SolrCoreMetricManager.REPLICA_TYPE_ATTR;
@@ -111,7 +109,6 @@ import
org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager.SnapshotMetaDa
import org.apache.solr.handler.IndexFetcher;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.SolrConfigHandler;
-import org.apache.solr.handler.admin.api.ReplicationAPIBase;
import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.handler.component.HighlightComponent;
import org.apache.solr.handler.component.SearchComponent;
@@ -128,19 +125,9 @@ import org.apache.solr.pkg.SolrPackageLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.request.SolrRequestInfo;
-import org.apache.solr.response.CSVResponseWriter;
-import org.apache.solr.response.CborResponseWriter;
-import org.apache.solr.response.GeoJSONResponseWriter;
-import org.apache.solr.response.GraphMLResponseWriter;
-import org.apache.solr.response.JacksonJsonWriter;
-import org.apache.solr.response.JavaBinResponseWriter;
-import org.apache.solr.response.PrometheusResponseWriter;
import org.apache.solr.response.QueryResponseWriter;
-import org.apache.solr.response.RawResponseWriter;
-import org.apache.solr.response.SchemaXmlResponseWriter;
-import org.apache.solr.response.SmileResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.response.XMLResponseWriter;
import org.apache.solr.response.transform.TransformerFactory;
import org.apache.solr.rest.ManagedResourceStorage;
import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
@@ -3085,51 +3072,6 @@ public class SolrCore implements SolrInfoBean, Closeable
{
private final PluginBag<QueryResponseWriter> responseWriters =
new PluginBag<>(QueryResponseWriter.class, this);
- public static final Map<String, QueryResponseWriter>
DEFAULT_RESPONSE_WRITERS;
-
- static {
- HashMap<String, QueryResponseWriter> m = new HashMap<>(15, 1);
- m.put("xml", new XMLResponseWriter());
- m.put(CommonParams.JSON, new JacksonJsonWriter());
- m.put("standard", m.get(CommonParams.JSON));
- m.put("geojson", new GeoJSONResponseWriter());
- m.put("graphml", new GraphMLResponseWriter());
- m.put("raw", new RawResponseWriter());
- m.put(CommonParams.JAVABIN, new JavaBinResponseWriter());
- m.put("cbor", new CborResponseWriter());
- m.put("csv", new CSVResponseWriter());
- m.put("schema.xml", new SchemaXmlResponseWriter());
- m.put("smile", new SmileResponseWriter());
- m.put(PROMETHEUS_METRICS_WT, new PrometheusResponseWriter());
- m.put(OPEN_METRICS_WT, new PrometheusResponseWriter());
- m.put(ReplicationAPIBase.FILE_STREAM, getFileStreamWriter());
- DEFAULT_RESPONSE_WRITERS = Collections.unmodifiableMap(m);
- }
-
- private static JavaBinResponseWriter getFileStreamWriter() {
- return new JavaBinResponseWriter() {
- @Override
- public void write(
- OutputStream out, SolrQueryRequest req, SolrQueryResponse response,
String contentType)
- throws IOException {
- RawWriter rawWriter = (RawWriter)
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
- if (rawWriter != null) {
- rawWriter.write(out);
- if (rawWriter instanceof Closeable) ((Closeable) rawWriter).close();
- }
- }
-
- @Override
- public String getContentType(SolrQueryRequest request, SolrQueryResponse
response) {
- RawWriter rawWriter = (RawWriter)
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
- if (rawWriter != null) {
- return rawWriter.getContentType();
- } else {
- return JavaBinResponseParser.JAVABIN_CONTENT_TYPE;
- }
- }
- };
- }
public void fetchLatestSchema() {
IndexSchema schema = configSet.getIndexSchema(true);
@@ -3145,11 +3087,48 @@ public class SolrCore implements SolrInfoBean,
Closeable {
}
/**
- * Configure the query response writers. There will always be a default
writer; additional writers
- * may also be configured.
+ * Gets a response writer suitable for node/container-level requests.
+ *
+ * @param writerName the writer name, or null for default
+ * @return the response writer, never null
+ * @deprecated Use {@link ResponseWritersRegistry#getWriter(String)} instead.
+ */
+ @Deprecated
+ public static QueryResponseWriter getAdminResponseWriter(String writerName) {
+ return ResponseWritersRegistry.getWriter(writerName);
+ }
+
+ /**
+ * Initializes query response writers. Response writers from {@code
ImplicitPlugins.json} may also
+ * be configured.
*/
private void initWriters() {
- responseWriters.init(DEFAULT_RESPONSE_WRITERS, this);
+ // Build default writers map from implicit plugins
+ Map<String, QueryResponseWriter> defaultWriters = new HashMap<>();
+
+ // Start with built-in writers that are always available
+ defaultWriters.putAll(ResponseWritersRegistry.getAllWriters());
+
+ // Load writers from ImplicitPlugins.json (may override built-ins)
+ List<PluginInfo> implicitWriters = getImplicitResponseWriters();
+ for (PluginInfo info : implicitWriters) {
+ try {
+ QueryResponseWriter writer =
+ createInstance(
+ info.className,
+ QueryResponseWriter.class,
+ "queryResponseWriter",
+ null,
+ getResourceLoader());
+ defaultWriters.put(info.name, writer);
+ } catch (Exception e) {
+ log.warn("Failed to load implicit response writer: {}", info.name, e);
+ }
+ }
+
+ // Initialize with the built defaults
+ responseWriters.init(defaultWriters, this);
+
// configure the default response writer; this one should never be null
if (responseWriters.getDefault() == null)
responseWriters.setDefault("standard");
}
@@ -3611,32 +3590,49 @@ public class SolrCore implements SolrInfoBean,
Closeable {
}
}
- private static final class ImplicitHolder {
- private ImplicitHolder() {}
+ private static final class ImplicitPluginsHolder {
+ private ImplicitPluginsHolder() {}
- private static final List<PluginInfo> INSTANCE;
+ private static final Map<String, List<PluginInfo>> ALL_IMPLICIT_PLUGINS;
static {
@SuppressWarnings("unchecked")
Map<String, ?> implicitPluginsInfo =
(Map<String, ?>)
Utils.fromJSONResource(SolrCore.class.getClassLoader(),
"ImplicitPlugins.json");
- @SuppressWarnings("unchecked")
- Map<String, Map<String, Object>> requestHandlers =
- (Map<String, Map<String, Object>>)
implicitPluginsInfo.get(SolrRequestHandler.TYPE);
- List<PluginInfo> implicits = new ArrayList<>(requestHandlers.size());
- for (Map.Entry<String, Map<String, Object>> entry :
requestHandlers.entrySet()) {
- Map<String, Object> info = entry.getValue();
- info.put(CommonParams.NAME, entry.getKey());
- implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info));
+ Map<String, List<PluginInfo>> plugins = new HashMap<>();
+
+ // Load all plugin types from the JSON
+ for (Map.Entry<String, ?> entry : implicitPluginsInfo.entrySet()) {
+ String pluginType = entry.getKey();
+ @SuppressWarnings("unchecked")
+ Map<String, Map<String, Object>> pluginConfigs =
+ (Map<String, Map<String, Object>>) entry.getValue();
+
+ List<PluginInfo> pluginInfos = new ArrayList<>(pluginConfigs.size());
+ for (Map.Entry<String, Map<String, Object>> plugin :
pluginConfigs.entrySet()) {
+ Map<String, Object> info = plugin.getValue();
+ info.put(CommonParams.NAME, plugin.getKey());
+ pluginInfos.add(new PluginInfo(pluginType, info));
+ }
+ plugins.put(pluginType, Collections.unmodifiableList(pluginInfos));
}
- INSTANCE = Collections.unmodifiableList(implicits);
+
+ ALL_IMPLICIT_PLUGINS = Collections.unmodifiableMap(plugins);
+ }
+
+ public static List<PluginInfo> getImplicitPlugins(String type) {
+ return ALL_IMPLICIT_PLUGINS.getOrDefault(type, Collections.emptyList());
}
}
public List<PluginInfo> getImplicitHandlers() {
- return ImplicitHolder.INSTANCE;
+ return ImplicitPluginsHolder.getImplicitPlugins(SolrRequestHandler.TYPE);
+ }
+
+ public List<PluginInfo> getImplicitResponseWriters() {
+ return ImplicitPluginsHolder.getImplicitPlugins("queryResponseWriter");
}
public CancellableQueryTracker getCancellableQueryTracker() {
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
index 16e78ab4268..18447eafac9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/SystemInfoHandler.java
@@ -77,9 +77,8 @@ public class SystemInfoHandler extends RequestHandlerBase {
private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
- * Undocumented expert level system property to prevent doing a reverse
lookup of our hostname.
- * This property will be logged as a suggested workaround if any problems
are noticed when doing
- * reverse lookup.
+ * Expert level system property to prevent doing a reverse lookup of our
hostname. This property
+ * will be logged as a suggested workaround if any problems are noticed when
doing reverse lookup.
*
* <p>TODO: should we refactor this (and the associated logic) into a helper
method for any other
* places where DNS is used?
@@ -97,7 +96,7 @@ public class SystemInfoHandler extends RequestHandlerBase {
private static final ConcurrentMap<Class<?>, BeanInfo> beanInfos = new
ConcurrentHashMap<>();
// on some platforms, resolving canonical hostname can cause the thread
- // to block for several seconds if nameservices aren't available
+ // to block for several seconds if name services aren't available
// so resolve this once per handler instance
// (ie: not static, so core reload will refresh)
private String hostname = null;
diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
index 10115192fba..0ce4d82e551 100644
--- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
+++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
@@ -31,6 +31,7 @@ import org.apache.solr.common.util.EnvUtils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.servlet.HttpSolrCall;
@@ -117,7 +118,7 @@ public interface SolrQueryRequest extends AutoCloseable {
/** The index searcher associated with this request */
SolrIndexSearcher getSearcher();
- /** The solr core (coordinator, etc) associated with this request */
+ /** The solr core (coordinator, etc.) associated with this request */
SolrCore getCore();
/** The schema snapshot from core.getLatestSchema() at request creation. */
@@ -145,7 +146,7 @@ public interface SolrQueryRequest extends AutoCloseable {
/**
* Only for V2 API. Returns a map of path segments and their values. For
example, if the path is
- * configured as /path/{segment1}/{segment2} and a reguest is made as
/path/x/y the returned map
+ * configured as /path/{segment1}/{segment2} and a request is made as
/path/x/y the returned map
* would contain {segment1:x ,segment2:y}
*/
default Map<String, String> getPathTemplateValues() {
@@ -195,16 +196,21 @@ public interface SolrQueryRequest extends AutoCloseable {
return getCore().getCoreDescriptor().getCloudDescriptor();
}
- /** The writer to use for this request, considering {@link CommonParams#WT}.
Never null. */
+ /**
+ * The writer to use for this request, considering {@link CommonParams#WT}.
Never null.
+ *
+ * <p>If a core is available, uses the core's response writer registry. If
no core is available
+ * (e.g., for node/container requests), uses a minimal set of
node/container-appropriate writers.
+ */
default QueryResponseWriter getResponseWriter() {
// it's weird this method is here instead of SolrQueryResponse, but it's
practical/convenient
SolrCore core = getCore();
String wt = getParams().get(CommonParams.WT);
+ // Use core writers if available, otherwise fall back to built-in writers
if (core != null) {
return core.getQueryResponseWriter(wt);
} else {
- return SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(
- wt, SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard"));
+ return ResponseWritersRegistry.getWriter(wt);
}
}
diff --git
a/solr/core/src/java/org/apache/solr/response/FileStreamResponseWriter.java
b/solr/core/src/java/org/apache/solr/response/FileStreamResponseWriter.java
new file mode 100644
index 00000000000..91f6ee12f33
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/FileStreamResponseWriter.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.response;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.solr.client.solrj.response.JavaBinResponseParser;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+import org.apache.solr.request.SolrQueryRequest;
+
+/**
+ * Response writer for file streaming operations, used for replication,
exports, and other core Solr
+ * operations.
+ *
+ * <p>This writer handles streaming of large files (such as index files) by
looking for a {@link
+ * org.apache.solr.core.SolrCore.RawWriter} object in the response under the
{@link
+ * ReplicationAPIBase#FILE_STREAM} key. When found, it delegates directly to
the raw writer to
+ * stream the file content efficiently.
+ *
+ * <p>This writer is specifically designed for replication file transfers and
provides no fallback
+ * behavior - it only works when a proper RawWriter is present in the response.
+ */
+public class FileStreamResponseWriter implements QueryResponseWriter {
+
+ @Override
+ public void write(
+ OutputStream out, SolrQueryRequest request, SolrQueryResponse response,
String contentType)
+ throws IOException {
+ SolrCore.RawWriter rawWriter =
+ (SolrCore.RawWriter)
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
+ if (rawWriter != null) {
+ rawWriter.write(out);
+ if (rawWriter instanceof Closeable closeable) {
+ closeable.close();
+ }
+ }
+ }
+
+ @Override
+ public String getContentType(SolrQueryRequest request, SolrQueryResponse
response) {
+ SolrCore.RawWriter rawWriter =
+ (SolrCore.RawWriter)
response.getValues().get(ReplicationAPIBase.FILE_STREAM);
+ if (rawWriter != null) {
+ String contentType = rawWriter.getContentType();
+ if (contentType != null) {
+ return contentType;
+ }
+ }
+ return JavaBinResponseParser.JAVABIN_CONTENT_TYPE;
+ }
+}
diff --git
a/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
new file mode 100644
index 00000000000..cfd04e28714
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.response;
+
+import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT;
+import static
org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT;
+
+import java.util.Map;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+
+/**
+ * Essential response writers always available regardless of core
configuration.
+ *
+ * <p>Used by node/container-level requests that have no associated {@link
+ * org.apache.solr.core.SolrCore}.
+ *
+ * <p>For the full set of response writers see {@link
org.apache.solr.core.SolrCore}'s response
+ * writer registry.
+ */
+public class ResponseWritersRegistry {
+
+ private ResponseWritersRegistry() {
+ // Prevent instantiation
+ }
+
+ private static final Map<String, QueryResponseWriter> BUILTIN_WRITERS;
+
+ static {
+ // Initialize built-in writers that are always available
+ JacksonJsonWriter jsonWriter = new JacksonJsonWriter();
+ PrometheusResponseWriter prometheusWriter = new PrometheusResponseWriter();
+
+ BUILTIN_WRITERS =
+ Map.of(
+ CommonParams.JAVABIN,
+ new JavaBinResponseWriter(),
+ CommonParams.JSON,
+ jsonWriter,
+ "standard",
+ jsonWriter, // Alias for JSON
+ "xml",
+ new XMLResponseWriter(),
+ PROMETHEUS_METRICS_WT,
+ prometheusWriter,
+ OPEN_METRICS_WT,
+ prometheusWriter,
+ ReplicationAPIBase.FILE_STREAM,
+ new FileStreamResponseWriter());
+ }
+
+ /**
+ * Gets a built-in response writer.
+ *
+ * <p>Built-in writers are always available and provide essential formats
needed by admin APIs and
+ * core functionality. They do not depend on core configuration or
ImplicitPlugins.json settings.
+ *
+ * <p>If the requested writer is not available, returns the "standard"
(JSON) writer as a
+ * fallback. This ensures requests always get a valid response format.
+ *
+ * @param writerName the writer name (e.g., "json", "xml", "javabin"), or
null for default
+ * @return the response writer, never null (returns "standard"/JSON if not
found)
+ */
+ public static QueryResponseWriter getWriter(String writerName) {
+ if (writerName == null || writerName.isEmpty()) {
+ return BUILTIN_WRITERS.get("standard");
+ }
+ return BUILTIN_WRITERS.getOrDefault(writerName,
BUILTIN_WRITERS.get("standard"));
+ }
+
+ /**
+ * Gets all built-in response writers.
+ *
+ * @return immutable map of all built-in writers
+ */
+ public static Map<String, QueryResponseWriter> getAllWriters() {
+ return BUILTIN_WRITERS;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
index f799859a263..5f2b67622d6 100644
--- a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
+++ b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
@@ -349,7 +349,7 @@ public class SolrQueryResponse {
*
* @param name the name of the header
* @param value the header value If it contains octet string, it should be
encoded according to
- * RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt)
+ * RFC 2047 (<a href="http://www.ietf.org/rfc/rfc2047.txt">...</a>)
* @see #addHttpHeader
* @see HttpServletResponse#setHeader
*/
@@ -364,7 +364,7 @@ public class SolrQueryResponse {
*
* @param name the name of the header
* @param value the additional header value If it contains octet string, it
should be encoded
- * according to RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt)
+ * according to RFC 2047 (<a
href="http://www.ietf.org/rfc/rfc2047.txt">...</a>)
* @see #setHttpHeader
* @see HttpServletResponse#addHeader
*/
diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
index 108b3f637f6..72c99a9df29 100644
--- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
+++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
@@ -86,6 +86,7 @@ import org.apache.solr.request.SolrQueryRequestBase;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuditEvent;
import org.apache.solr.security.AuditEvent.EventType;
@@ -740,8 +741,9 @@ public class HttpSolrCall {
solrResp.getToLogAsString("[admin]"));
}
}
+ // node/container requests have no core, use built-in writers
QueryResponseWriter respWriter =
-
SolrCore.DEFAULT_RESPONSE_WRITERS.get(solrReq.getParams().get(CommonParams.WT));
+
ResponseWritersRegistry.getWriter(solrReq.getParams().get(CommonParams.WT));
if (respWriter == null) respWriter = getResponseWriter();
writeResponse(solrResp, respWriter, Method.getMethod(req.getMethod()));
if (shouldAudit()) {
diff --git a/solr/core/src/resources/ImplicitPlugins.json
b/solr/core/src/resources/ImplicitPlugins.json
index 4154e70ded9..eba9bd05ba0 100644
--- a/solr/core/src/resources/ImplicitPlugins.json
+++ b/solr/core/src/resources/ImplicitPlugins.json
@@ -157,5 +157,25 @@
"activetaskslist"
]
}
+ },
+ "queryResponseWriter": {
+ "geojson": {
+ "class": "solr.GeoJSONResponseWriter"
+ },
+ "graphml": {
+ "class": "solr.GraphMLResponseWriter"
+ },
+ "cbor": {
+ "class": "solr.CborResponseWriter"
+ },
+ "csv": {
+ "class": "solr.CSVResponseWriter"
+ },
+ "schema.xml": {
+ "class": "solr.SchemaXmlResponseWriter"
+ },
+ "smile": {
+ "class": "solr.SmileResponseWriter"
+ }
}
}
diff --git a/solr/core/src/test/org/apache/solr/core/TestImplicitPlugins.java
b/solr/core/src/test/org/apache/solr/core/TestImplicitPlugins.java
new file mode 100644
index 00000000000..a04604b22d7
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/core/TestImplicitPlugins.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.core;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.ResponseWritersRegistry;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests for implicit plugins loaded from ImplicitPlugins.json.
+ *
+ * <p>This test class verifies:
+ *
+ * <ul>
+ * <li>Request handlers are loaded from ImplicitPlugins.json
+ * <li>Response writers are loaded from ImplicitPlugins.json for core
requests
+ * <li>Built in response writers use a minimal set defined in {@link
ResponseWritersRegistry}.
+ * </ul>
+ */
+public class TestImplicitPlugins extends SolrTestCaseJ4 {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig.xml", "schema.xml");
+ }
+
+ // ========== Core vs Built-in Writer Separation Tests ==========
+
+ @Test
+ public void testCoreAndBuiltInWriterIntegration() {
+ final SolrCore core = h.getCore();
+
+ // Test that core has extended writers from ImplicitPlugins.json
+ assertNotNull("Core should have csv writer",
core.getQueryResponseWriter("csv"));
+ assertNotNull("Core should have geojson writer",
core.getQueryResponseWriter("geojson"));
+ assertNotNull("Core should have graphml writer",
core.getQueryResponseWriter("graphml"));
+ assertNotNull("Core should have smile writer",
core.getQueryResponseWriter("smile"));
+
+ // Test that built-in registry has minimal set and falls back for extended
formats
+ QueryResponseWriter standardWriter =
ResponseWritersRegistry.getWriter("standard");
+ assertSame(
+ "Built-in csv request should fall back to standard",
+ standardWriter,
+ ResponseWritersRegistry.getWriter("csv"));
+ assertSame(
+ "Built-in geojson request should fall back to standard",
+ standardWriter,
+ ResponseWritersRegistry.getWriter("geojson"));
+
+ // Test that both systems have common essential formats (though may be
different instances)
+ QueryResponseWriter coreJsonWriter =
core.getQueryResponseWriter(CommonParams.JSON);
+ QueryResponseWriter builtInJsonWriter =
ResponseWritersRegistry.getWriter(CommonParams.JSON);
+ assertNotNull("Core json writer should not be null", coreJsonWriter);
+ assertNotNull("Built-in json writer should not be null",
builtInJsonWriter);
+
+ QueryResponseWriter coreXmlWriter = core.getQueryResponseWriter("xml");
+ QueryResponseWriter builtInXmlWriter =
ResponseWritersRegistry.getWriter("xml");
+ assertNotNull("Core xml writer should not be null", coreXmlWriter);
+ assertNotNull("Built-in xml writer should not be null", builtInXmlWriter);
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/response/TestFileStreamResponseWriter.java
b/solr/core/src/test/org/apache/solr/response/TestFileStreamResponseWriter.java
new file mode 100644
index 00000000000..5852aefbe14
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/response/TestFileStreamResponseWriter.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.response;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.response.JavaBinResponseParser;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.admin.api.ReplicationAPIBase;
+import org.apache.solr.request.LocalSolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequest;
+import org.junit.Test;
+
+public class TestFileStreamResponseWriter extends SolrTestCase {
+
+ @Test
+ public void testWriteWithRawWriter() throws IOException {
+ FileStreamResponseWriter writer = new FileStreamResponseWriter();
+ SolrQueryRequest request = new LocalSolrQueryRequest(null, new
ModifiableSolrParams());
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ // Create a mock RawWriter
+ String testContent = "test file content";
+ TestRawWriter rawWriter = new TestRawWriter(testContent,
"application/octet-stream");
+
+ // Add the RawWriter to the response
+ response.add(ReplicationAPIBase.FILE_STREAM, rawWriter);
+
+ // Write to output stream
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(out, request, response, null);
+
+ // Verify the content was written
+ String written = out.toString(StandardCharsets.UTF_8);
+ assertEquals("Content should be written directly", testContent, written);
+ }
+
+ @Test
+ public void testWriteWithoutRawWriter() throws IOException {
+ FileStreamResponseWriter writer = new FileStreamResponseWriter();
+ SolrQueryRequest request = new LocalSolrQueryRequest(null, new
ModifiableSolrParams());
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ // Don't add any RawWriter to the response
+
+ // Write to output stream
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(out, request, response, null);
+
+ // Verify nothing was written (since no RawWriter present)
+ assertEquals("Nothing should be written when no RawWriter present", 0,
out.size());
+ }
+
+ @Test
+ public void testGetContentTypeWithRawWriter() {
+ FileStreamResponseWriter writer = new FileStreamResponseWriter();
+ SolrQueryRequest request = new LocalSolrQueryRequest(null, new
ModifiableSolrParams());
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ // Create a mock RawWriter with custom content type
+ String customContentType = "application/custom-type";
+ TestRawWriter rawWriter = new TestRawWriter("content", customContentType);
+
+ // Add the RawWriter to the response
+ response.add(ReplicationAPIBase.FILE_STREAM, rawWriter);
+
+ // Get content type
+ String contentType = writer.getContentType(request, response);
+ assertEquals("Should return RawWriter's content type", customContentType,
contentType);
+ }
+
+ @Test
+ public void testGetContentTypeWithoutRawWriter() {
+ FileStreamResponseWriter writer = new FileStreamResponseWriter();
+ SolrQueryRequest request = new LocalSolrQueryRequest(null, new
ModifiableSolrParams());
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ // Don't add any RawWriter to the response
+
+ // Get content type
+ String contentType = writer.getContentType(request, response);
+ assertEquals(
+ "Should return default javabin content type",
+ JavaBinResponseParser.JAVABIN_CONTENT_TYPE,
+ contentType);
+ }
+
+ @Test
+ public void testGetContentTypeWithRawWriterReturningNull() {
+ FileStreamResponseWriter writer = new FileStreamResponseWriter();
+ SolrQueryRequest request = new LocalSolrQueryRequest(null, new
ModifiableSolrParams());
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ // Create a mock RawWriter that returns null for content type
+ TestRawWriter rawWriter = new TestRawWriter("content", null);
+
+ // Add the RawWriter to the response
+ response.add(ReplicationAPIBase.FILE_STREAM, rawWriter);
+
+ // Get content type
+ String contentType = writer.getContentType(request, response);
+ assertEquals(
+ "Should return default javabin content type when RawWriter returns
null",
+ JavaBinResponseParser.JAVABIN_CONTENT_TYPE,
+ contentType);
+ }
+
+ // Test helper classes
+ // Avoids standing up a full Solr core for this test by mocking.
+ private static class TestRawWriter implements SolrCore.RawWriter {
+ private final String content;
+ private final String contentType;
+
+ public TestRawWriter(String content, String contentType) {
+ this.content = content;
+ this.contentType = contentType;
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public void write(OutputStream os) throws IOException {
+ if (content != null) {
+ os.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ }
+}
diff --git
a/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
new file mode 100644
index 00000000000..695ad0e1278
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.response;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+/**
+ * This test validates the registry's behavior for built-in response writers,
including
+ * availability, fallback behavior, and proper format handling. Notice there
is no core configured!
+ */
+public class TestResponseWritersRegistry extends SolrTestCaseJ4 {
+
+ @Test
+ public void testBuiltInWriterFallbackBehavior() {
+ QueryResponseWriter standardWriter =
ResponseWritersRegistry.getWriter("standard");
+
+ // Test null fallback
+ QueryResponseWriter nullWriter = ResponseWritersRegistry.getWriter(null);
+ assertThat("null writer should not be null", nullWriter,
is(not(nullValue())));
+ assertThat("null writer should be same as standard", nullWriter,
is(standardWriter));
+
+ // Test empty string fallback
+ QueryResponseWriter emptyWriter = ResponseWritersRegistry.getWriter("");
+ assertThat("empty writer should not be null", emptyWriter,
is(not(nullValue())));
+ assertThat("empty writer should be same as standard", emptyWriter,
is(standardWriter));
+
+ // Test unknown format fallback
+ QueryResponseWriter unknownWriter =
ResponseWritersRegistry.getWriter("nonexistent");
+ assertThat("unknown writer should not be null", unknownWriter,
is(not(nullValue())));
+ assertThat("unknown writer should be same as standard", unknownWriter,
is(standardWriter));
+ }
+
+ @Test
+ public void testBuiltInWriterLimitedSet() {
+ QueryResponseWriter standardWriter =
ResponseWritersRegistry.getWriter("standard");
+
+ // Built-in writers should NOT include extended format writers (csv,
geojson, etc.)
+ // These should all fall back to standard
+ // I think this standard thing is weird... I think it should throw an
exception!
+ assertThat(
+ "geojson should fall back to standard",
+ ResponseWritersRegistry.getWriter("geojson"),
+ is(standardWriter));
+ }
+}