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.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 24c7dc83b0c..bab4359d87d 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; @@ -112,7 +110,6 @@ 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; @@ -129,19 +126,9 @@ 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; @@ -3088,51 +3075,6 @@ public PluginBag getResponseWriters() { private final PluginBag responseWriters = new PluginBag<>(QueryResponseWriter.class, this); - public static final Map DEFAULT_RESPONSE_WRITERS; - - static { - HashMap 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); @@ -3148,11 +3090,48 @@ default String getContentType() { } /** - * 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 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 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"); } @@ -3614,32 +3593,49 @@ public void cleanupOldIndexDirectories(boolean reload) { } } - private static final class ImplicitHolder { - private ImplicitHolder() {} + private static final class ImplicitPluginsHolder { + private ImplicitPluginsHolder() {} - private static final List INSTANCE; + private static final Map> ALL_IMPLICIT_PLUGINS; static { @SuppressWarnings("unchecked") Map implicitPluginsInfo = (Map) Utils.fromJSONResource(SolrCore.class.getClassLoader(), "ImplicitPlugins.json"); - @SuppressWarnings("unchecked") - Map> requestHandlers = - (Map>) implicitPluginsInfo.get(SolrRequestHandler.TYPE); - List implicits = new ArrayList<>(requestHandlers.size()); - for (Map.Entry> entry : requestHandlers.entrySet()) { - Map info = entry.getValue(); - info.put(CommonParams.NAME, entry.getKey()); - implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info)); + Map> plugins = new HashMap<>(); + + // Load all plugin types from the JSON + for (Map.Entry entry : implicitPluginsInfo.entrySet()) { + String pluginType = entry.getKey(); + @SuppressWarnings("unchecked") + Map> pluginConfigs = + (Map>) entry.getValue(); + + List pluginInfos = new ArrayList<>(pluginConfigs.size()); + for (Map.Entry> plugin : pluginConfigs.entrySet()) { + Map 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 getImplicitPlugins(String type) { + return ALL_IMPLICIT_PLUGINS.getOrDefault(type, Collections.emptyList()); } } public List getImplicitHandlers() { - return ImplicitHolder.INSTANCE; + return ImplicitPluginsHolder.getImplicitPlugins(SolrRequestHandler.TYPE); + } + + public List 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. * *

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, 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.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 @@ static boolean disallowPartialResults(SolrParams params) { /** 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 @@ default String getPath() { /** * 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 getPathTemplateValues() { @@ -195,16 +196,21 @@ default CloudDescriptor getCloudDescriptor() { 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. + * + *

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. + * + *

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. + * + *

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. + * + *

Used by node/container-level requests that have no associated {@link + * org.apache.solr.core.SolrCore}. + * + *

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 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. + * + *

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. + * + *

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 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 boolean isHttpCaching() { * * @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 (...) * @see #addHttpHeader * @see HttpServletResponse#setHeader */ @@ -364,7 +364,7 @@ public void setHttpHeader(String name, String value) { * * @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 (...) * @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 db998ff9b8c..1229aed8d0a 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.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; @@ -735,8 +736,9 @@ protected void logAndFlushAdminRequest(SolrQueryResponse solrResp) throws IOExce 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. + * + *

This test class verifies: + * + *

    + *
  • Request handlers are loaded from ImplicitPlugins.json + *
  • Response writers are loaded from ImplicitPlugins.json for core requests + *
  • Built in response writers use a minimal set defined in {@link ResponseWritersRegistry}. + *
+ */ +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)); + } +}