You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/03/15 13:46:10 UTC
[solr] 01/02: SOLR-16372: Migrate collection listing, deletion to JAX-RS (#1412)
This is an automated email from the ASF dual-hosted git repository.
gerlowskija pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
commit e53cb0bc8d08fd5d259c7a731b9f2c769a51b253
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Mar 9 10:22:43 2023 -0500
SOLR-16372: Migrate collection listing, deletion to JAX-RS (#1412)
The endpoints themselves remain the same, other than moving to the
JAX-RS framework.
---
.../src/java/org/apache/solr/api/V2HttpCall.java | 7 +-
.../org/apache/solr/handler/CollectionsAPI.java | 15 ----
.../solr/handler/admin/CollectionsHandler.java | 34 ++++----
.../handler/admin/api/DeleteCollectionAPI.java | 95 +++++++++++++++++-----
.../solr/handler/admin/api/ListCollectionsAPI.java | 75 +++++++++++++++++
.../org/apache/solr/handler/api/V2ApiUtils.java | 20 +++++
.../solr/jersey/CatchAllExceptionMapper.java | 22 +----
.../org/apache/solr/jersey/InjectionFactories.java | 1 +
.../org/apache/solr/jersey/JerseyApplications.java | 6 +-
.../solr/jersey/MediaTypeOverridingFilter.java | 69 ++++++++++++++++
.../org/apache/solr/jersey/SolrJacksonMapper.java | 31 ++++++-
.../SubResponseAccumulatingJerseyResponse.java | 55 +++++++++++++
.../apache/solr/handler/V2ApiIntegrationTest.java | 4 +-
.../solr/handler/admin/TestApiFramework.java | 1 -
.../solr/handler/admin/TestCollectionAPIs.java | 3 -
.../handler/admin/V2CollectionsAPIMappingTest.java | 8 --
.../handler/admin/api/DeleteCollectionAPITest.java | 62 ++++++++++++++
.../admin/api/V2CollectionAPIMappingTest.java | 1 -
.../solr/client/solrj/request/TestV2Request.java | 7 +-
19 files changed, 424 insertions(+), 92 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
index 1493a032036..ad8c4308fe6 100644
--- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -140,7 +140,7 @@ public class V2HttpCall extends HttpSolrCall {
assert core == null;
}
- if ("c".equals(prefix) || "collections".equals(prefix)) {
+ if (pathSegments.size() > 1 && ("c".equals(prefix) || "collections".equals(prefix))) {
origCorename = pathSegments.get(1);
DocCollection collection =
@@ -151,6 +151,11 @@ public class V2HttpCall extends HttpSolrCall {
SolrException.ErrorCode.BAD_REQUEST, "no such collection or alias");
}
} else {
+ // Certain HTTP methods are only used for admin APIs, check for those and short-circuit
+ if (List.of("delete").contains(req.getMethod().toLowerCase(Locale.ROOT))) {
+ initAdminRequest(path);
+ return;
+ }
boolean isPreferLeader = (path.endsWith("/update") || path.contains("/update/"));
core = getCoreByCollection(collection.getName(), isPreferLeader);
if (core == null) {
diff --git a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
index 11f1ee5df4a..a2b76ced5b8 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -17,7 +17,6 @@
package org.apache.solr.handler;
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.ROUTER_KEY;
import static org.apache.solr.cloud.api.collections.RoutedAlias.CREATE_COLLECTION_PREFIX;
@@ -27,9 +26,7 @@ import static org.apache.solr.common.params.CommonParams.ACTION;
import static org.apache.solr.handler.ClusterAPI.wrapParams;
import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
-import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
-import com.google.common.collect.Maps;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -49,8 +46,6 @@ import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
import org.apache.solr.common.params.CollectionAdminParams;
import org.apache.solr.common.params.CollectionParams.CollectionAction;
import org.apache.solr.handler.admin.CollectionsHandler;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
/** All V2 APIs for collection management */
public class CollectionsAPI {
@@ -70,16 +65,6 @@ public class CollectionsAPI {
this.collectionsHandler = collectionsHandler;
}
- @EndPoint(
- path = {"/c", "/collections"},
- method = GET,
- permission = COLL_READ_PERM)
- public void getCollections(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- final Map<String, Object> v1Params = Maps.newHashMap();
- v1Params.put(ACTION, CollectionAction.LIST.toLower());
- collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
- }
-
@EndPoint(
path = {"/c", "/collections"},
method = POST,
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
index e59ce947cb5..42430bf39e7 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionsHandler.java
@@ -218,6 +218,7 @@ import org.apache.solr.handler.admin.api.DeleteReplicaPropertyAPI;
import org.apache.solr.handler.admin.api.DeleteShardAPI;
import org.apache.solr.handler.admin.api.ForceLeaderAPI;
import org.apache.solr.handler.admin.api.ListAliasesAPI;
+import org.apache.solr.handler.admin.api.ListCollectionsAPI;
import org.apache.solr.handler.admin.api.MigrateDocsAPI;
import org.apache.solr.handler.admin.api.ModifyCollectionAPI;
import org.apache.solr.handler.admin.api.MoveReplicaAPI;
@@ -666,8 +667,17 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
DELETE_OP(
DELETE,
(req, rsp, h) -> {
- Map<String, Object> map = copy(req.getParams().required(), null, NAME);
- return copy(req.getParams(), map, FOLLOW_ALIASES);
+ final RequiredSolrParams requiredParams = req.getParams().required();
+ final DeleteCollectionAPI deleteCollectionAPI =
+ new DeleteCollectionAPI(h.coreContainer, req, rsp);
+ final SolrJerseyResponse deleteCollResponse =
+ deleteCollectionAPI.deleteCollection(
+ requiredParams.get(NAME),
+ req.getParams().getBool(FOLLOW_ALIASES),
+ req.getParams().get(ASYNC));
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, deleteCollResponse);
+
+ return null;
}),
// XXX should this command support followAliases?
RELOAD_OP(
@@ -1232,19 +1242,10 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
LIST_OP(
LIST,
(req, rsp, h) -> {
- NamedList<Object> results = new NamedList<>();
- Map<String, DocCollection> collections =
- h.coreContainer
- .getZkController()
- .getZkStateReader()
- .getClusterState()
- .getCollectionsMap();
- List<String> collectionList = new ArrayList<>(collections.keySet());
- Collections.sort(collectionList);
- // XXX should we add aliases here?
- results.add("collections", collectionList);
- SolrResponse response = new OverseerSolrResponse(results);
- rsp.getValues().addAll(response.getResponse());
+ final ListCollectionsAPI listCollectionsAPI =
+ new ListCollectionsAPI(h.coreContainer, req, rsp);
+ final SolrJerseyResponse listCollectionsResponse = listCollectionsAPI.listCollections();
+ V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, listCollectionsResponse);
return null;
}),
/**
@@ -2072,7 +2073,9 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
return List.of(
AddReplicaPropertyAPI.class,
+ DeleteCollectionAPI.class,
DeleteReplicaPropertyAPI.class,
+ ListCollectionsAPI.class,
ReplaceNodeAPI.class,
DeleteNodeAPI.class,
ListAliasesAPI.class);
@@ -2089,7 +2092,6 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
apis.addAll(AnnotatedApi.getApis(new ForceLeaderAPI(this)));
apis.addAll(AnnotatedApi.getApis(new DeleteReplicaAPI(this)));
apis.addAll(AnnotatedApi.getApis(new BalanceShardUniqueAPI(this)));
- apis.addAll(AnnotatedApi.getApis(new DeleteCollectionAPI(this)));
apis.addAll(AnnotatedApi.getApis(new MigrateDocsAPI(this)));
apis.addAll(AnnotatedApi.getApis(new ModifyCollectionAPI(this)));
apis.addAll(AnnotatedApi.getApis(new MoveReplicaAPI(this)));
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
index 80e4ff192db..2c38da38a87 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteCollectionAPI.java
@@ -16,16 +16,29 @@
*/
package org.apache.solr.handler.admin.api;
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-import static org.apache.solr.common.params.CommonParams.ACTION;
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
+import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.common.cloud.ZkStateReader;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
@@ -35,26 +48,64 @@ import org.apache.solr.response.SolrQueryResponse;
* <p>This API (DELETE /v2/collections/collectionName) is equivalent to the v1
* /admin/collections?action=DELETE command.
*/
-public class DeleteCollectionAPI {
+@Path("collections/")
+public class DeleteCollectionAPI extends AdminAPIBase {
- private final CollectionsHandler collectionsHandler;
+ @Inject
+ public DeleteCollectionAPI(
+ CoreContainer coreContainer,
+ SolrQueryRequest solrQueryRequest,
+ SolrQueryResponse solrQueryResponse) {
+ super(coreContainer, solrQueryRequest, solrQueryResponse);
+ }
+
+ @DELETE
+ @Path("{collectionName}")
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ @PermissionName(COLL_EDIT_PERM)
+ public SubResponseAccumulatingJerseyResponse deleteCollection(
+ @PathParam("collectionName") String collectionName,
+ @QueryParam("followAliases") Boolean followAliases,
+ @QueryParam("async") String asyncId)
+ throws Exception {
+ final SubResponseAccumulatingJerseyResponse response =
+ instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+ final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+ recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+ final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, followAliases, asyncId);
+ final SolrResponse remoteResponse =
+ CollectionsHandler.submitCollectionApiCommand(
+ coreContainer,
+ coreContainer.getDistributedCollectionCommandRunner(),
+ remoteMessage,
+ CollectionParams.CollectionAction.DELETE,
+ DEFAULT_COLLECTION_OP_TIMEOUT);
+ if (remoteResponse.getException() != null) {
+ throw remoteResponse.getException();
+ }
+
+ if (asyncId != null) {
+ response.requestId = asyncId;
+ return response;
+ }
- public DeleteCollectionAPI(CollectionsHandler collectionsHandler) {
- this.collectionsHandler = collectionsHandler;
+ // Values fetched from remoteResponse may be null
+ response.successfulSubResponsesByNodeName = remoteResponse.getResponse().get("success");
+ response.failedSubResponsesByNodeName = remoteResponse.getResponse().get("failure");
+
+ return response;
}
- @EndPoint(
- path = {"/c/{collection}", "/collections/{collection}"},
- method = DELETE,
- permission = COLL_EDIT_PERM)
- public void deleteCollection(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
- req =
- wrapParams(
- req,
- ACTION,
- CollectionParams.CollectionAction.DELETE.toString(),
- NAME,
- req.getPathTemplateValues().get(ZkStateReader.COLLECTION_PROP));
- collectionsHandler.handleRequestBody(req, rsp);
+ public static ZkNodeProps createRemoteMessage(
+ String collectionName, Boolean followAliases, String asyncId) {
+ final Map<String, Object> remoteMessage = new HashMap<>();
+
+ remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETE.toLower());
+ remoteMessage.put(NAME, collectionName);
+ if (followAliases != null) remoteMessage.put(FOLLOW_ALIASES, followAliases);
+ if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
+
+ return new ZkNodeProps(remoteMessage);
}
}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java
new file mode 100644
index 00000000000..df4fc547bf2
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/ListCollectionsAPI.java
@@ -0,0 +1,75 @@
+/*
+ * 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.handler.admin.api;
+
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for listing collections.
+ *
+ * <p>This API (GET /v2/collections) is equivalent to the v1 /admin/collections?action=LIST command
+ */
+@Path("/collections")
+public class ListCollectionsAPI extends AdminAPIBase {
+
+ @Inject
+ public ListCollectionsAPI(
+ CoreContainer coreContainer, SolrQueryRequest req, SolrQueryResponse rsp) {
+ super(coreContainer, req, rsp);
+ }
+
+ @GET
+ @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+ @PermissionName(COLL_READ_PERM)
+ public ListCollectionsResponse listCollections() {
+ final ListCollectionsResponse response =
+ instantiateJerseyResponse(ListCollectionsResponse.class);
+ validateZooKeeperAwareCoreContainer(coreContainer);
+
+ Map<String, DocCollection> collections =
+ coreContainer.getZkController().getZkStateReader().getClusterState().getCollectionsMap();
+ List<String> collectionList = new ArrayList<>(collections.keySet());
+ Collections.sort(collectionList);
+ // XXX should we add aliases here?
+ response.collections = collectionList;
+
+ return response;
+ }
+
+ public static class ListCollectionsResponse extends SolrJerseyResponse {
+ @JsonProperty("collections")
+ public List<String> collections;
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
index d459a85caf6..300082406e7 100644
--- a/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
+++ b/solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
@@ -17,12 +17,16 @@
package org.apache.solr.handler.api;
+import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static org.apache.solr.common.params.CommonParams.WT;
+
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.apache.solr.common.MapWriter.EntryWriter;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
/** Utilities helpful for common V2 API declaration tasks. */
@@ -81,6 +85,22 @@ public class V2ApiUtils {
squashIntoNamedList(destination, mw, false);
}
+ public static String getMediaTypeFromWtParam(
+ SolrQueryRequest solrQueryRequest, String defaultMediaType) {
+ final String wtParam = solrQueryRequest.getParams().get(WT);
+ if (wtParam == null) return "application/json";
+
+ // The only currently-supported response-formats for JAX-RS v2 endpoints.
+ switch (wtParam) {
+ case "xml":
+ return "application/xml";
+ case "javabin":
+ return BINARY_CONTENT_TYPE_V2;
+ default:
+ return defaultMediaType;
+ }
+ }
+
private static void squashIntoNamedList(
NamedList<Object> destination, JacksonReflectMapWriter mw, boolean trimHeader) {
try {
diff --git a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
index 2ff8253216d..6dfec0fe1ba 100644
--- a/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/CatchAllExceptionMapper.java
@@ -17,9 +17,7 @@
package org.apache.solr.jersey;
-import static org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
import static org.apache.solr.common.SolrException.ErrorCode.getErrorCode;
-import static org.apache.solr.common.params.CommonParams.WT;
import static org.apache.solr.jersey.RequestContextKeys.HANDLER_METRICS;
import static org.apache.solr.jersey.RequestContextKeys.SOLR_JERSEY_RESPONSE;
import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_REQUEST;
@@ -30,10 +28,12 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ResourceContext;
import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import org.apache.solr.common.SolrException;
import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.servlet.ResponseUtils;
@@ -110,25 +110,11 @@ public class CatchAllExceptionMapper implements ExceptionMapper<Exception> {
response.error = ResponseUtils.getTypedErrorInfo(normalizedException, log);
response.responseHeader.status = response.error.code;
- final String mediaType = getMediaType(solrQueryRequest);
+ final String mediaType =
+ V2ApiUtils.getMediaTypeFromWtParam(solrQueryRequest, MediaType.APPLICATION_JSON);
return Response.status(response.error.code).type(mediaType).entity(response).build();
}
- private static String getMediaType(SolrQueryRequest solrQueryRequest) {
- final String wtParam = solrQueryRequest.getParams().get(WT);
- if (wtParam == null) return "application/json";
-
- // The only currently-supported response-formats for JAX-RS v2 endpoints.
- switch (wtParam) {
- case "xml":
- return "application/xml";
- case "javabin":
- return BINARY_CONTENT_TYPE_V2;
- default:
- return "application/json";
- }
- }
-
private Response processWebApplicationException(WebApplicationException wae) {
return wae.getResponse();
}
diff --git a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
index 758f32adbe3..2cbc5f99cfd 100644
--- a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
+++ b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
@@ -27,6 +27,7 @@ import org.apache.solr.response.SolrQueryResponse;
import org.glassfish.hk2.api.Factory;
public class InjectionFactories {
+
public static class SolrQueryRequestFactory implements Factory<SolrQueryRequest> {
private final ContainerRequestContext containerRequestContext;
diff --git a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
index cb6b5bf0cde..ef9587ceb90 100644
--- a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
+++ b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
@@ -62,6 +62,7 @@ public class JerseyApplications {
// Request lifecycle logic
register(CatchAllExceptionMapper.class);
register(NotFoundExceptionMapper.class);
+ register(MediaTypeOverridingFilter.class);
register(RequestMetricHandling.PreRequestMetricsFilter.class);
register(RequestMetricHandling.PostRequestMetricsFilter.class);
register(PostRequestDecorationFilter.class);
@@ -84,9 +85,12 @@ public class JerseyApplications {
}
});
+ // Explicit Jersey logging is disabled by default but useful for debugging (pt 1)
+ // register(LoggingFeature.class);
+
setProperties(
Map.of(
- // Explicit Jersey logging is disabled by default but useful for debugging
+ // Explicit Jersey logging is disabled by default but useful for debugging (pt 2)
// "jersey.config.server.tracing.type", "ALL",
// "jersey.config.server.tracing.threshold", "VERBOSE",
"jersey.config.server.wadl.disableWadl", "true",
diff --git a/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java b/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java
new file mode 100644
index 00000000000..00cda1f08a9
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/MediaTypeOverridingFilter.java
@@ -0,0 +1,69 @@
+/*
+ * 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.jersey;
+
+import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
+import static org.apache.solr.jersey.RequestContextKeys.SOLR_QUERY_REQUEST;
+
+import java.io.IOException;
+import java.util.List;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.core.Context;
+import org.apache.solr.api.JerseyResource;
+import org.apache.solr.handler.admin.ZookeeperReadAPI;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.request.SolrQueryRequest;
+
+// TODO Deprecate or remove support for the 'wt' parameter in the v2 APIs in favor of the more
+// HTTP-compliant 'Accept' header
+/** Overrides the content-type of the response based on an optional user-provided 'wt' parameter */
+public class MediaTypeOverridingFilter implements ContainerResponseFilter {
+
+ private static final List<Class<? extends JerseyResource>> EXEMPTED_RESOURCES =
+ List.of(ZookeeperReadAPI.class);
+
+ @Context private ResourceInfo resourceInfo;
+
+ @Override
+ public void filter(
+ ContainerRequestContext requestContext, ContainerResponseContext responseContext)
+ throws IOException {
+
+ // Solr has historically ignored 'wt' for client or server error responses, so maintain that
+ // behavior here for compatibility.
+ if (responseContext.getStatus() >= 400) {
+ return;
+ }
+
+ // Some endpoints have their own media-type logic and opt out of the overriding behavior this
+ // filter provides.
+ if (EXEMPTED_RESOURCES.contains(resourceInfo.getResourceClass())) {
+ return;
+ }
+
+ final SolrQueryRequest solrQueryRequest =
+ (SolrQueryRequest) requestContext.getProperty(SOLR_QUERY_REQUEST);
+ final String mediaType = V2ApiUtils.getMediaTypeFromWtParam(solrQueryRequest, null);
+ if (mediaType != null) {
+ responseContext.getHeaders().putSingle(CONTENT_TYPE, mediaType);
+ }
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
index 94d8e090b21..00513d6ce82 100644
--- a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
@@ -18,15 +18,44 @@
package org.apache.solr.jersey;
import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import java.io.IOException;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
+import org.apache.solr.common.util.NamedList;
/** Customizes the ObjectMapper settings used for serialization/deserialization in Jersey */
+@SuppressWarnings("rawtypes")
@Provider
public class SolrJacksonMapper implements ContextResolver<ObjectMapper> {
@Override
public ObjectMapper getContext(Class<?> type) {
- return new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ final SimpleModule customTypeModule = new SimpleModule();
+ customTypeModule.addSerializer(new NamedListSerializer(NamedList.class));
+
+ return new ObjectMapper()
+ .setSerializationInclusion(JsonInclude.Include.NON_NULL)
+ .registerModule(customTypeModule);
+ }
+
+ public static class NamedListSerializer extends StdSerializer<NamedList> {
+
+ public NamedListSerializer() {
+ this(null);
+ }
+
+ public NamedListSerializer(Class<NamedList> nlClazz) {
+ super(nlClazz);
+ }
+
+ @Override
+ public void serialize(NamedList value, JsonGenerator gen, SerializerProvider provider)
+ throws IOException {
+ gen.writeObject(value.asShallowMap());
+ }
}
}
diff --git a/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java b/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
new file mode 100644
index 00000000000..ab476771d75
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
@@ -0,0 +1,55 @@
+/*
+ * 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.jersey;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents API responses composed of the responses of various sub-requests.
+ *
+ * <p>Many Solr APIs, particularly those historically reliant on overseer processing, return a
+ * response to the user that is composed in large part of the responses from all sub-requests made
+ * during the APIs execution. (e.g. the collection-deletion response itself contains the responses
+ * from the 'UNLOAD' call send to each core.) This class encapsulates those responses as possible.
+ */
+public class SubResponseAccumulatingJerseyResponse extends SolrJerseyResponse {
+
+ @JsonProperty("requestid")
+ public String requestId;
+
+ // TODO The 'Object' value in this and the failure prop below have a more defined structure.
+ // Specifically, each value is a map whose keys are node names and whose values are full
+ // responses (in NamedList form) of all shard or replica requests made to that node by the
+ // overseer. We've skipped being more explicit here, type-wise, for a few reasons:
+ // 1. While the overseer response comes back as a raw NamedList, there's no good way to
+ // serialize it into a more strongly-typed response without some ugly NL inspection code
+ // 2. The overseer response can include duplicate keys when multiple replica-requests are sent
+ // by the overseer to the same node. This makes the overseer response invalid JSON, and
+ // prevents utilizing Jackson for serde.
+ // 3. This would still all be surmountable if the user response for overseer-based APIs was
+ // especially worth preserving, but it's not. We should rework this response format to be
+ // less verbose in the successful case and to be more explicit in the failure case about
+ // which internal replica requests failed.
+ // We should either change this response format to be more helpful, or add stronger typing to
+ // overseer responses so that being more type-explicit here is feasible.
+ @JsonProperty("success")
+ public Object successfulSubResponsesByNodeName;
+
+ @JsonProperty("failure")
+ public Object failedSubResponsesByNodeName;
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
index 893125713f1..6c904312160 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -77,7 +77,9 @@ public class V2ApiIntegrationTest extends SolrCloudTestCase {
BaseHttpSolrClient.RemoteSolrException ex =
expectThrows(
BaseHttpSolrClient.RemoteSolrException.class,
- () -> v2Request.process(cluster.getSolrClient()));
+ () -> {
+ v2Request.process(cluster.getSolrClient());
+ });
assertEquals(expectedCode, ex.code());
}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
index c5352514356..af2a4e0c590 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestApiFramework.java
@@ -137,7 +137,6 @@ public class TestApiFramework extends SolrTestCaseJ4 {
methodNames.add(rsp.getValues()._getStr("/spec[0]/methods[0]", null));
methodNames.add(rsp.getValues()._getStr("/spec[1]/methods[0]", null));
methodNames.add(rsp.getValues()._getStr("/spec[2]/methods[0]", null));
- assertTrue(methodNames.contains("DELETE"));
assertTrue(methodNames.contains("POST"));
assertTrue(methodNames.contains("GET"));
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
index e074f0ba40d..680eaa95747 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/TestCollectionAPIs.java
@@ -139,9 +139,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
compareOutput(
apiBag, "/collections/collName", POST, "{reload:{}}", "{name:collName, operation :reload}");
- compareOutput(
- apiBag, "/collections/collName", DELETE, null, "{name:collName, operation :delete}");
-
compareOutput(
apiBag,
"/collections/collName/shards/shard1",
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
index 230bf604f73..2949fd38a67 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/V2CollectionsAPIMappingTest.java
@@ -113,14 +113,6 @@ public class V2CollectionsAPIMappingTest extends V2ApiMappingTest<CollectionsHan
assertEquals(1, v1Params.getPrimitiveInt(CollectionAdminParams.NUM_SHARDS));
}
- @Test
- public void testListCollectionsAllProperties() throws Exception {
- final String noBody = null;
- final SolrParams v1Params = captureConvertedV1Params("/collections", "GET", noBody);
-
- assertEquals(CollectionParams.CollectionAction.LIST.lowerName, v1Params.get(ACTION));
- }
-
@Test
public void testCreateAliasAllProperties() throws Exception {
final SolrParams v1Params =
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
new file mode 100644
index 00000000000..a87dad3c900
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteCollectionAPITest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.handler.admin.api;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static org.apache.solr.common.params.CollectionParams.NAME;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteCollectionAPI} */
+public class DeleteCollectionAPITest extends SolrTestCaseJ4 {
+
+ @Test
+ public void testConstructsValidOverseerMessage() {
+ // Only required properties provided
+ {
+ final ZkNodeProps message =
+ DeleteCollectionAPI.createRemoteMessage("someCollName", null, null);
+ final Map<String, Object> rawMessage = message.getProperties();
+ assertEquals(2, rawMessage.size());
+ MatcherAssert.assertThat(rawMessage.keySet(), containsInAnyOrder(QUEUE_OPERATION, NAME));
+ assertEquals("delete", rawMessage.get(QUEUE_OPERATION));
+ assertEquals("someCollName", rawMessage.get(NAME));
+ }
+
+ // Optional properties ('followAliases' and 'async') also provided
+ {
+ final ZkNodeProps message =
+ DeleteCollectionAPI.createRemoteMessage("someCollName", Boolean.TRUE, "someAsyncId");
+ final Map<String, Object> rawMessage = message.getProperties();
+ assertEquals(4, rawMessage.size());
+ MatcherAssert.assertThat(
+ rawMessage.keySet(), containsInAnyOrder(QUEUE_OPERATION, NAME, ASYNC, FOLLOW_ALIASES));
+ assertEquals("delete", rawMessage.get(QUEUE_OPERATION));
+ assertEquals("someCollName", rawMessage.get(NAME));
+ assertEquals(Boolean.TRUE, rawMessage.get(FOLLOW_ALIASES));
+ assertEquals("someAsyncId", rawMessage.get(ASYNC));
+ }
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
index 65feab737db..e2ead1e452d 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionAPIMappingTest.java
@@ -55,7 +55,6 @@ public class V2CollectionAPIMappingTest extends V2ApiMappingTest<CollectionsHand
public void populateApiBag() {
final CollectionsHandler collectionsHandler = getRequestHandler();
apiBag.registerObject(new BalanceShardUniqueAPI(collectionsHandler));
- apiBag.registerObject(new DeleteCollectionAPI(collectionsHandler));
apiBag.registerObject(new MigrateDocsAPI(collectionsHandler));
apiBag.registerObject(new ModifyCollectionAPI(collectionsHandler));
apiBag.registerObject(new MoveReplicaAPI(collectionsHandler));
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java
index 315bced3ee0..ab8c81c7f24 100644
--- a/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/TestV2Request.java
@@ -112,8 +112,6 @@ public class TestV2Request extends SolrCloudTestCase {
+ "}"
+ "/* ignore comment*/")
.build());
- assertSuccess(client, new V2Request.Builder("/c").build());
- assertSuccess(client, new V2Request.Builder("/c/_introspect").build());
String requestHandlerName = "/x" + random().nextInt();
assertSuccess(
@@ -127,8 +125,9 @@ public class TestV2Request extends SolrCloudTestCase {
.build());
assertSuccess(
- client, new V2Request.Builder("/c/test").withMethod(SolrRequest.METHOD.DELETE).build());
- NamedList<Object> res = client.request(new V2Request.Builder("/c").build());
+ client,
+ new V2Request.Builder("/collections/test").withMethod(SolrRequest.METHOD.DELETE).build());
+ NamedList<Object> res = client.request(new V2Request.Builder("/collections").build());
// TODO: this is not guaranteed now - beast test if you try to fix
// List collections = (List) res.get("collections");