Skip to content

Commit

Permalink
SOLR-16396: Convert v2 configset APIs to JAX-RS (#2928)
Browse files Browse the repository at this point in the history
Convert v2 configset APIs to JAX-RS.  Naturally this adds the APIs to
Solr's growing v2 OAS, and ensures the APIs are included in code-gen
artifacts.

Also makes slight tweaks to line APIs up with the more REST-ful design
chosen for other v2 APIs.

  - 'clone' (i.e. 'create-from-existing') is now available at `POST
    /api/configsets {...}`
  - 'delete' is now available at `DELETE /api/configsets/csName`
  - 'list' is now available at `GET /api/configsets`
  • Loading branch information
gerlowskija authored Jan 3, 2025
1 parent d0897fc commit b2d18ca
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 312 deletions.
4 changes: 4 additions & 0 deletions solr/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ Improvements
specific collections or cores. Collection information can be fetched by a call to `GET /api/collections/collectionName`, and core
information with a call to `GET /api/cores/coreName/segments`. (Jason Gerlowski)

* SOLR-16396: All v2 configset APIs have been moved to the slightly different path: `/api/configsets`, to better align with the design of
other v2 APIs. SolrJ now offers (experimental) SolrRequest implementations for all v2 configset APIs in
`org.apache.solr.client.solrj.request.ConfigsetsApi`. (Jason Gerlowski)

Optimizations
---------------------
* SOLR-17578: Remove ZkController internal core supplier, for slightly faster reconnection after Zookeeper session loss. (Pierre Salagnac)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.client.api.endpoint;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import java.io.IOException;
import java.io.InputStream;
import org.apache.solr.client.api.model.CloneConfigsetRequestBody;
import org.apache.solr.client.api.model.ListConfigsetsResponse;
import org.apache.solr.client.api.model.SolrJerseyResponse;

public interface ConfigsetsApi {

/** V2 API definition for listing the configsets available to this SolrCloud cluster. */
@Path("/configsets")
interface List {
@GET
@Operation(
summary = "List the configsets available to Solr.",
tags = {"configsets"})
ListConfigsetsResponse listConfigSet() throws Exception;
}

/**
* V2 API definition for creating a (possibly slightly modified) copy of an existing configset
*
* <p>Equivalent to the existing v1 API /admin/configs?action=CREATE
*/
@Path("/configsets")
interface Clone {
@POST
@Operation(
summary = "Create a new configset modeled on an existing one.",
tags = {"configsets"})
SolrJerseyResponse cloneExistingConfigSet(CloneConfigsetRequestBody requestBody)
throws Exception;
}

/**
* V2 API definition for deleting an existing configset.
*
* <p>Equivalent to the existing v1 API /admin/configs?action=DELETE
*/
@Path("/configsets/{configSetName}")
interface Delete {
@DELETE
@Operation(summary = "Delete an existing configset.", tags = "configsets")
SolrJerseyResponse deleteConfigSet(@PathParam("configSetName") String configSetName)
throws Exception;
}

/**
* V2 API definitions for uploading a configset, in whole or part.
*
* <p>Equivalent to the existing v1 API /admin/configs?action=UPLOAD
*/
@Path("/configsets/{configSetName}")
interface Upload {
@PUT
@Operation(summary = "Create a new configset.", tags = "configsets")
SolrJerseyResponse uploadConfigSet(
@PathParam("configSetName") String configSetName,
@QueryParam("overwrite") Boolean overwrite,
@QueryParam("cleanup") Boolean cleanup,
@RequestBody(required = true) InputStream requestBody)
throws IOException;

@PUT
@Path("{filePath:.+}")
SolrJerseyResponse uploadConfigSetFile(
@PathParam("configSetName") String configSetName,
@PathParam("filePath") String filePath,
@QueryParam("overwrite") Boolean overwrite,
@QueryParam("cleanup") Boolean cleanup,
@RequestBody(required = true) InputStream requestBody)
throws IOException;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.client.solrj.request.beans;
package org.apache.solr.client.api.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.util.ReflectMapWriter;

public class CreateConfigPayload implements ReflectMapWriter {
public static final String DEFAULT_CONFIGSET =
"_default"; // TODO Better location for this in SolrJ?
/** Request body for ConfigsetsApi.Clone */
public class CloneConfigsetRequestBody {
public static final String DEFAULT_CONFIGSET = "_default";

@JsonProperty(required = true)
public String name;

@JsonProperty public String baseConfigSet = DEFAULT_CONFIGSET;
@JsonProperty(defaultValue = DEFAULT_CONFIGSET)
public String baseConfigSet;

@JsonProperty public Map<String, Object> properties;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,30 @@
package org.apache.solr.handler.admin;

import static org.apache.solr.common.params.CommonParams.NAME;
import static org.apache.solr.handler.configsets.UploadConfigSetFileAPI.FILEPATH_PLACEHOLDER;

import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
import org.apache.solr.api.JerseyResource;
import org.apache.solr.api.PayloadObj;
import org.apache.solr.client.solrj.request.beans.CreateConfigPayload;
import org.apache.solr.client.api.model.CloneConfigsetRequestBody;
import org.apache.solr.client.api.model.SolrJerseyResponse;
import org.apache.solr.cloud.ConfigSetCmds;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.ConfigSetParams;
import org.apache.solr.common.params.ConfigSetParams.ConfigSetAction;
import org.apache.solr.common.params.DefaultSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.handler.configsets.CreateConfigSetAPI;
import org.apache.solr.handler.configsets.DeleteConfigSetAPI;
import org.apache.solr.handler.configsets.CloneConfigSet;
import org.apache.solr.handler.configsets.ConfigSetAPIBase;
import org.apache.solr.handler.configsets.DeleteConfigSet;
import org.apache.solr.handler.configsets.ListConfigSets;
import org.apache.solr.handler.configsets.UploadConfigSetAPI;
import org.apache.solr.handler.configsets.UploadConfigSetFileAPI;
import org.apache.solr.request.DelegatingSolrQueryRequest;
import org.apache.solr.handler.configsets.UploadConfigSet;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
Expand Down Expand Up @@ -96,51 +90,30 @@ public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throw

switch (action) {
case DELETE:
final DeleteConfigSetAPI deleteConfigSetAPI = new DeleteConfigSetAPI(coreContainer);
final SolrQueryRequest v2DeleteReq =
new DelegatingSolrQueryRequest(req) {
@Override
public Map<String, String> getPathTemplateValues() {
return Map.of(
DeleteConfigSetAPI.CONFIGSET_NAME_PLACEHOLDER,
req.getParams().required().get(NAME));
}
};
deleteConfigSetAPI.deleteConfigSet(v2DeleteReq, rsp);
final DeleteConfigSet deleteConfigSetAPI = new DeleteConfigSet(coreContainer, req, rsp);
final var deleteResponse =
deleteConfigSetAPI.deleteConfigSet(req.getParams().required().get(NAME));
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, deleteResponse);
break;
case UPLOAD:
final SolrQueryRequest v2UploadReq =
new DelegatingSolrQueryRequest(req) {
@Override
public Map<String, String> getPathTemplateValues() {
final Map<String, String> templateValsByName = new HashMap<>();

templateValsByName.put(
UploadConfigSetAPI.CONFIGSET_NAME_PLACEHOLDER,
req.getParams().required().get(NAME));
if (!req.getParams().get(ConfigSetParams.FILE_PATH, "").isEmpty()) {
templateValsByName.put(
FILEPATH_PLACEHOLDER, req.getParams().get(ConfigSetParams.FILE_PATH));
}
return templateValsByName;
}

// Set the v1 default vals where they differ from v2's
@Override
public SolrParams getParams() {
final ModifiableSolrParams v1Defaults = new ModifiableSolrParams();
v1Defaults.add(ConfigSetParams.OVERWRITE, "false");
v1Defaults.add(ConfigSetParams.CLEANUP, "false");
return new DefaultSolrParams(super.getParams(), v1Defaults);
}
};
final var uploadApi = new UploadConfigSet(coreContainer, req, rsp);
final var configSetName = req.getParams().required().get(NAME);
final var overwrite = req.getParams().getBool(ConfigSetParams.OVERWRITE, false);
final var cleanup = req.getParams().getBool(ConfigSetParams.CLEANUP, false);
final var configSetData = ConfigSetAPIBase.ensureNonEmptyInputStream(req);
SolrJerseyResponse uploadResponse;
if (req.getParams()
.get(ConfigSetParams.FILE_PATH, "")
.isEmpty()) { // Uploading a whole configset
new UploadConfigSetAPI(coreContainer).uploadConfigSet(v2UploadReq, rsp);
uploadResponse =
uploadApi.uploadConfigSet(configSetName, overwrite, cleanup, configSetData);
} else { // Uploading a single file
new UploadConfigSetFileAPI(coreContainer).updateConfigSetFile(v2UploadReq, rsp);
final var filePath = req.getParams().get(ConfigSetParams.FILE_PATH);
uploadResponse =
uploadApi.uploadConfigSetFile(
configSetName, filePath, overwrite, cleanup, configSetData);
}
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, uploadResponse);
break;
case LIST:
final ListConfigSets listConfigSetsAPI = new ListConfigSets(coreContainer);
Expand All @@ -153,12 +126,14 @@ public SolrParams getParams() {
}

// Map v1 parameters into v2 format and process request
final CreateConfigPayload createPayload = new CreateConfigPayload();
createPayload.name = newConfigSetName;
final var requestBody = new CloneConfigsetRequestBody();
requestBody.name = newConfigSetName;
if (req.getParams().get(ConfigSetCmds.BASE_CONFIGSET) != null) {
createPayload.baseConfigSet = req.getParams().get(ConfigSetCmds.BASE_CONFIGSET);
requestBody.baseConfigSet = req.getParams().get(ConfigSetCmds.BASE_CONFIGSET);
} else {
requestBody.baseConfigSet = "_default";
}
createPayload.properties = new HashMap<>();
requestBody.properties = new HashMap<>();
req.getParams().stream()
.filter(entry -> entry.getKey().startsWith(ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX))
.forEach(
Expand All @@ -167,10 +142,11 @@ public SolrParams getParams() {
entry.getKey().substring(ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX.length());
final Object value =
(entry.getValue().length == 1) ? entry.getValue()[0] : entry.getValue();
createPayload.properties.put(newKey, value);
requestBody.properties.put(newKey, value);
});
final CreateConfigSetAPI createConfigSetAPI = new CreateConfigSetAPI(coreContainer);
createConfigSetAPI.create(new PayloadObj<>("create", null, createPayload, req, rsp));
final CloneConfigSet createConfigSetAPI = new CloneConfigSet(coreContainer, req, rsp);
final var createResponse = createConfigSetAPI.cloneExistingConfigSet(requestBody);
V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, createResponse);
break;
default:
throw new IllegalStateException("Unexpected ConfigSetAction detected: " + action);
Expand Down Expand Up @@ -207,18 +183,13 @@ public Boolean registerV2() {

@Override
public Collection<Api> getApis() {
final List<Api> apis = new ArrayList<>();
apis.addAll(AnnotatedApi.getApis(new CreateConfigSetAPI(coreContainer)));
apis.addAll(AnnotatedApi.getApis(new DeleteConfigSetAPI(coreContainer)));
apis.addAll(AnnotatedApi.getApis(new UploadConfigSetAPI(coreContainer)));
apis.addAll(AnnotatedApi.getApis(new UploadConfigSetFileAPI(coreContainer)));

return apis;
return new ArrayList<>();
}

@Override
public Collection<Class<? extends JerseyResource>> getJerseyResources() {
return List.of(ListConfigSets.class);
return List.of(
ListConfigSets.class, CloneConfigSet.class, DeleteConfigSet.class, UploadConfigSet.class);
}

@Override
Expand Down
Loading

0 comments on commit b2d18ca

Please sign in to comment.