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 2dc430b6aff..46c66e403df 100644 --- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java +++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java @@ -138,7 +138,7 @@ public void call(SolrQueryRequest req, SolrQueryResponse rsp) { 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 = @@ -149,6 +149,11 @@ public void call(SolrQueryRequest req, SolrQueryResponse rsp) { 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.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.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 CollectionsAPI(CollectionsHandler collectionsHandler) { this.collectionsHandler = collectionsHandler; } - @EndPoint( - path = {"/c", "/collections"}, - method = GET, - permission = COLL_READ_PERM) - public void getCollections(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { - final Map 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 374373f8da6..f5110687344 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 @@ -217,6 +217,7 @@ 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; @@ -664,8 +665,17 @@ public enum CollectionOperation implements CollectionOp { DELETE_OP( DELETE, (req, rsp, h) -> { - Map 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( @@ -1231,19 +1241,10 @@ public Map execute( LIST_OP( LIST, (req, rsp, h) -> { - NamedList results = new NamedList<>(); - Map collections = - h.coreContainer - .getZkController() - .getZkStateReader() - .getClusterState() - .getCollectionsMap(); - List 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; }), /** @@ -2067,7 +2068,9 @@ public Boolean registerV2() { public Collection> getJerseyResources() { return List.of( AddReplicaPropertyAPI.class, + DeleteCollectionAPI.class, DeleteReplicaPropertyAPI.class, + ListCollectionsAPI.class, ReplaceNodeAPI.class, DeleteNodeAPI.class, ListAliasesAPI.class); @@ -2084,7 +2087,6 @@ public Collection getApis() { 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 @@ *

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

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 collections = + coreContainer.getZkController().getZkStateReader().getClusterState().getCollectionsMap(); + List 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 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 static void squashIntoNamedList( 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 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.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 static Response buildExceptionResponse( 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.glassfish.hk2.api.Factory; public class InjectionFactories { + public static class SolrQueryRequestFactory implements Factory { 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 CoreContainerApp() { // 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 @@ protected void configure() { } }); + // 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> 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 { @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 { + + public NamedListSerializer() { + this(null); + } + + public NamedListSerializer(Class 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. + * + *

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 @@ private void testException( 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 void testFramework() { 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 void testCommands() throws Exception { 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 void testCreateCollectionAllProperties() throws Exception { 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 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 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 res = client.request(new V2Request.Builder("/c").build()); + client, + new V2Request.Builder("/collections/test").withMethod(SolrRequest.METHOD.DELETE).build()); + NamedList 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");