Skip to content

Commit

Permalink
SOLR-16372: Migrate collection listing, deletion to JAX-RS (#1412)
Browse files Browse the repository at this point in the history
The endpoints themselves remain the same, other than moving to the
JAX-RS framework.
  • Loading branch information
gerlowskija committed Mar 15, 2023
1 parent 34c85d3 commit e53cb0b
Show file tree
Hide file tree
Showing 19 changed files with 424 additions and 92 deletions.
7 changes: 6 additions & 1 deletion solr/core/src/java/org/apache/solr/api/V2HttpCall.java
Expand Up @@ -140,7 +140,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 =
Expand All @@ -151,6 +151,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) {
Expand Down
15 changes: 0 additions & 15 deletions solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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<String, Object> v1Params = Maps.newHashMap();
v1Params.put(ACTION, CollectionAction.LIST.toLower());
collectionsHandler.handleRequestBody(wrapParams(req, v1Params), rsp);
}

@EndPoint(
path = {"/c", "/collections"},
method = POST,
Expand Down
Expand Up @@ -218,6 +218,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;
Expand Down Expand Up @@ -666,8 +667,17 @@ public enum CollectionOperation implements CollectionOp {
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(
Expand Down Expand Up @@ -1232,19 +1242,10 @@ public Map<String, Object> execute(
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;
}),
/**
Expand Down Expand Up @@ -2072,7 +2073,9 @@ public Boolean registerV2() {
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
return List.of(
AddReplicaPropertyAPI.class,
DeleteCollectionAPI.class,
DeleteReplicaPropertyAPI.class,
ListCollectionsAPI.class,
ReplaceNodeAPI.class,
DeleteNodeAPI.class,
ListAliasesAPI.class);
Expand All @@ -2089,7 +2092,6 @@ public Collection<Api> 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)));
Expand Down
Expand Up @@ -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;

Expand All @@ -35,26 +48,64 @@
* <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);
}
}
@@ -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;
}
}
20 changes: 20 additions & 0 deletions solr/core/src/java/org/apache/solr/handler/api/V2ApiUtils.java
Expand Up @@ -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. */
Expand Down Expand Up @@ -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<Object> destination, JacksonReflectMapWriter mw, boolean trimHeader) {
try {
Expand Down

0 comments on commit e53cb0b

Please sign in to comment.