This is an automated email from the ASF dual-hosted git repository.

gerlowskija pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new fe4f669a3a1 SOLR-16394: Migrate coll backup creation to JAX-RS (#1541)
fe4f669a3a1 is described below

commit fe4f669a3a193b91533a3f8aa1650b10da0a0b3f
Author: Jason Gerlowski <[email protected]>
AuthorDate: Thu Apr 6 16:00:21 2023 -0400

    SOLR-16394: Migrate coll backup creation to JAX-RS (#1541)
    
    This commit makes various cosmetic improvements to Solr's v2
    (collection-level) create backup API, to bring it more into line with
    the more REST-ful v2 design.
    
    As of this commit, the create-backup API is now:
      - POST /api/collections/collName/backups/backName/versions
    
    It also migrates the API definition to JAX-RS.
---
 solr/CHANGES.txt                                   |   3 +
 .../cloud/api/collections/DeleteBackupCmd.java     |   3 +-
 .../org/apache/solr/handler/CollectionsAPI.java    |  11 -
 .../solr/handler/admin/CollectionsHandler.java     |  87 +------
 .../solr/handler/admin/api/AdminAPIBase.java       |  23 ++
 .../admin/api/CreateCollectionBackupAPI.java       | 267 +++++++++++++++++++++
 .../org/apache/solr/jersey/SolrJacksonMapper.java  |  11 +
 .../apache/solr/handler/V2ApiIntegrationTest.java  |   8 +-
 .../handler/admin/V2CollectionsAPIMappingTest.java |  30 ---
 .../admin/api/V2CollectionBackupApiTest.java       | 100 ++++++++
 .../pages/collection-management.adoc               |  35 ++-
 .../collections/AbstractIncrementalBackupTest.java |   4 +-
 12 files changed, 441 insertions(+), 141 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 11b8566c883..ced2d58539e 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -71,6 +71,9 @@ Improvements
   available under the `PUT` and `DELETE` verbs at 
`/api/collections/collName/properties/propName` depending on whether the 
property is
   being upserted or deleted. (Jason Gerlowski)
 
+* SOLR-16394: The path of the v2 "collection backup" API has been tweaked 
slightly to be more intuitive, and is now available at
+  `POST /api/collections/backups/backupName/versions`. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 
diff --git 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
index d930f69f583..72e1043a957 100644
--- 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
+++ 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/DeleteBackupCmd.java
@@ -40,6 +40,7 @@ import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.backup.AggregateBackupStats;
 import org.apache.solr.core.backup.BackupFilePaths;
@@ -237,7 +238,7 @@ public class DeleteBackupCmd implements 
CollApiCmds.CollectionApiCommand {
     List<NamedList<Object>> shardBackupIdDetails = new ArrayList<>();
     results.add("deleted", shardBackupIdDetails);
     for (BackupId backupId : backupIdDeletes) {
-      NamedList<Object> backupIdResult = new NamedList<>();
+      NamedList<Object> backupIdResult = new SimpleOrderedMap<>();
 
       try {
         BackupProperties props =
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 a07ef69ca9f..ac3c49d7a90 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -35,7 +35,6 @@ import java.util.stream.Collectors;
 import org.apache.solr.api.Command;
 import org.apache.solr.api.EndPoint;
 import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.BackupCollectionPayload;
 import org.apache.solr.client.solrj.request.beans.CreateAliasPayload;
 import org.apache.solr.client.solrj.request.beans.CreatePayload;
 import org.apache.solr.client.solrj.request.beans.RestoreCollectionPayload;
@@ -48,7 +47,6 @@ import org.apache.solr.handler.admin.CollectionsHandler;
 public class CollectionsAPI {
 
   public static final String V2_CREATE_COLLECTION_CMD = "create";
-  public static final String V2_BACKUP_CMD = "backup-collection";
   public static final String V2_RESTORE_CMD = "restore-collection";
   public static final String V2_CREATE_ALIAS_CMD = "create-alias";
 
@@ -66,15 +64,6 @@ public class CollectionsAPI {
       permission = COLL_EDIT_PERM)
   public class CollectionsCommands {
 
-    @Command(name = V2_BACKUP_CMD)
-    public void backupCollection(PayloadObj<BackupCollectionPayload> obj) 
throws Exception {
-      final Map<String, Object> v1Params = obj.get().toMap(new HashMap<>());
-      v1Params.put(ACTION, CollectionAction.BACKUP.toLower());
-
-      collectionsHandler.handleRequestBody(
-          wrapParams(obj.getRequest(), v1Params), obj.getResponse());
-    }
-
     @Command(name = V2_RESTORE_CMD)
     @SuppressWarnings("unchecked")
     public void restoreBackup(PayloadObj<RestoreCollectionPayload> obj) throws 
Exception {
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 67ae42ca38a..3bbbfa2c687 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
@@ -212,6 +212,7 @@ import org.apache.solr.handler.admin.api.AliasPropertyAPI;
 import org.apache.solr.handler.admin.api.BalanceShardUniqueAPI;
 import org.apache.solr.handler.admin.api.CollectionPropertyAPI;
 import org.apache.solr.handler.admin.api.CollectionStatusAPI;
+import org.apache.solr.handler.admin.api.CreateCollectionBackupAPI;
 import org.apache.solr.handler.admin.api.CreateShardAPI;
 import org.apache.solr.handler.admin.api.DeleteAliasAPI;
 import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
@@ -1389,87 +1390,10 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
     BACKUP_OP(
         BACKUP,
         (req, rsp, h) -> {
-          req.getParams().required().check(NAME, COLLECTION_PROP);
-
-          final String extCollectionName = 
req.getParams().get(COLLECTION_PROP);
-          final boolean followAliases = 
req.getParams().getBool(FOLLOW_ALIASES, false);
-          final String collectionName =
-              followAliases
-                  ? h.coreContainer
-                      .getZkController()
-                      .getZkStateReader()
-                      .getAliases()
-                      .resolveSimpleAlias(extCollectionName)
-                  : extCollectionName;
-          final ClusterState clusterState = 
h.coreContainer.getZkController().getClusterState();
-          if (!clusterState.hasCollection(collectionName)) {
-            throw new SolrException(
-                ErrorCode.BAD_REQUEST,
-                "Collection '" + collectionName + "' does not exist, no action 
taken.");
-          }
-
-          CoreContainer cc = h.coreContainer;
-          String repo = req.getParams().get(CoreAdminParams.BACKUP_REPOSITORY);
-          BackupRepository repository = cc.newBackupRepository(repo);
-
-          String location =
-              
repository.getBackupLocation(req.getParams().get(CoreAdminParams.BACKUP_LOCATION));
-          if (location == null) {
-            // Refresh the cluster property file to make sure the value set 
for location is the
-            // latest. Check if the location is specified in the cluster 
property.
-            location =
-                new 
ClusterProperties(h.coreContainer.getZkController().getZkClient())
-                    .getClusterProperty(CoreAdminParams.BACKUP_LOCATION, null);
-            if (location == null) {
-              throw new SolrException(
-                  ErrorCode.BAD_REQUEST,
-                  "'location' is not specified as a query"
-                      + " parameter or as a default repository property or as 
a cluster property.");
-            }
-          }
-          boolean incremental = 
req.getParams().getBool(CoreAdminParams.BACKUP_INCREMENTAL, true);
-
-          // Check if the specified location is valid for this repository.
-          final URI uri = repository.createDirectoryURI(location);
-          try {
-            if (!repository.exists(uri)) {
-              throw new SolrException(
-                  ErrorCode.SERVER_ERROR, "specified location " + uri + " does 
not exist.");
-            }
-          } catch (IOException ex) {
-            throw new SolrException(
-                ErrorCode.SERVER_ERROR,
-                "Failed to check the existence of " + uri + ". Is it valid?",
-                ex);
-          }
-
-          String strategy =
-              req.getParams()
-                  .get(
-                      CollectionAdminParams.INDEX_BACKUP_STRATEGY,
-                      CollectionAdminParams.COPY_FILES_STRATEGY);
-          if 
(!CollectionAdminParams.INDEX_BACKUP_STRATEGIES.contains(strategy)) {
-            throw new SolrException(
-                ErrorCode.BAD_REQUEST, "Unknown index backup strategy " + 
strategy);
-          }
-
-          Map<String, Object> params =
-              copy(
-                  req.getParams(),
-                  null,
-                  NAME,
-                  COLLECTION_PROP,
-                  FOLLOW_ALIASES,
-                  CoreAdminParams.COMMIT_NAME,
-                  CoreAdminParams.MAX_NUM_BACKUP_POINTS);
-          params.put(CoreAdminParams.BACKUP_LOCATION, location);
-          if (repo != null) {
-            params.put(CoreAdminParams.BACKUP_REPOSITORY, repo);
-          }
-
-          params.put(CollectionAdminParams.INDEX_BACKUP_STRATEGY, strategy);
-          params.put(CoreAdminParams.BACKUP_INCREMENTAL, incremental);
-          return params;
+          final var response =
+              CreateCollectionBackupAPI.invokeFromV1Params(req, rsp, 
h.coreContainer);
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
+          return null;
         }),
     RESTORE_OP(
         RESTORE,
@@ -2110,6 +2034,7 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
     return List.of(
         AddReplicaPropertyAPI.class,
+        CreateCollectionBackupAPI.class,
         DeleteAliasAPI.class,
         DeleteCollectionAPI.class,
         DeleteReplicaPropertyAPI.class,
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
index 16d4b189d02..4ffe8b83bbc 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AdminAPIBase.java
@@ -19,6 +19,7 @@ package org.apache.solr.handler.admin.api;
 
 import org.apache.solr.api.JerseyResource;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.logging.MDCLoggingContext;
 import org.apache.solr.request.SolrQueryRequest;
@@ -46,6 +47,28 @@ public abstract class AdminAPIBase extends JerseyResource {
     return coreContainer;
   }
 
+  protected String resolveAndValidateAliasIfEnabled(
+      String unresolvedCollectionName, boolean aliasResolutionEnabled) {
+    final String resolvedCollectionName =
+        aliasResolutionEnabled ? resolveAlias(unresolvedCollectionName) : 
unresolvedCollectionName;
+    final ClusterState clusterState = 
coreContainer.getZkController().getClusterState();
+    if (!clusterState.hasCollection(resolvedCollectionName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Collection '" + resolvedCollectionName + "' does not exist, no 
action taken.");
+    }
+
+    return resolvedCollectionName;
+  }
+
+  private String resolveAlias(String aliasName) {
+    return coreContainer
+        .getZkController()
+        .getZkStateReader()
+        .getAliases()
+        .resolveSimpleAlias(aliasName);
+  }
+
   public static void validateZooKeeperAwareCoreContainer(CoreContainer 
coreContainer) {
     if (coreContainer == null) {
       throw new SolrException(
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackupAPI.java
 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackupAPI.java
new file mode 100644
index 00000000000..cfb835ff3ff
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionBackupAPI.java
@@ -0,0 +1,267 @@
+/*
+ * 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.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
+import static 
org.apache.solr.common.params.CollectionAdminParams.FOLLOW_ALIASES;
+import static 
org.apache.solr.common.params.CollectionAdminParams.INDEX_BACKUP_STRATEGY;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.common.params.CoreAdminParams.BACKUP_INCREMENTAL;
+import static org.apache.solr.common.params.CoreAdminParams.BACKUP_LOCATION;
+import static org.apache.solr.common.params.CoreAdminParams.BACKUP_REPOSITORY;
+import static org.apache.solr.common.params.CoreAdminParams.COMMIT_NAME;
+import static 
org.apache.solr.common.params.CoreAdminParams.MAX_NUM_BACKUP_POINTS;
+import static 
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ClusterProperties;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.CollectionAdminParams;
+import org.apache.solr.common.params.CollectionParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.backup.repository.BackupRepository;
+import org.apache.solr.handler.admin.CollectionsHandler;
+import org.apache.solr.jersey.JacksonReflectMapWriter;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJacksonMapper;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.jersey.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.zookeeper.common.StringUtils;
+
+/**
+ * V2 API for creating a new "backup" of a specified collection
+ *
+ * <p>This API is analogous to the v1 /admin/collections?action=BACKUP command.
+ */
+@Path("/collections/{collectionName}/backups/{backupName}/versions")
+public class CreateCollectionBackupAPI extends AdminAPIBase {
+  private final ObjectMapper objectMapper;
+
+  @Inject
+  public CreateCollectionBackupAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+
+    this.objectMapper = SolrJacksonMapper.getObjectMapper();
+  }
+
+  @POST
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse createCollectionBackup(
+      @PathParam("collectionName") String collectionName,
+      @PathParam("backupName") String backupName,
+      CreateCollectionBackupRequestBody requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing 
required request body");
+    }
+    if (StringUtils.isBlank(backupName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter: 
'backupName'");
+    }
+    if (collectionName == null) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter: 
'collection'");
+    }
+    final CoreContainer coreContainer = 
fetchAndValidateZooKeeperAwareCoreContainer();
+    recordCollectionForLogAndTracing(collectionName, solrQueryRequest);
+
+    collectionName =
+        resolveAndValidateAliasIfEnabled(
+            collectionName, Boolean.TRUE.equals(requestBody.followAliases));
+
+    final BackupRepository repository = 
coreContainer.newBackupRepository(requestBody.repository);
+    requestBody.location = getLocation(repository, requestBody.location);
+    if (requestBody.incremental == null) {
+      requestBody.incremental = Boolean.TRUE;
+    }
+
+    // Check if the specified location is valid for this repository.
+    final URI uri = repository.createDirectoryURI(requestBody.location);
+    try {
+      if (!repository.exists(uri)) {
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR, "specified location " + uri 
+ " does not exist.");
+      }
+    } catch (IOException ex) {
+      throw new SolrException(
+          SolrException.ErrorCode.SERVER_ERROR,
+          "Failed to check the existence of " + uri + ". Is it valid?",
+          ex);
+    }
+
+    if (requestBody.backupStrategy == null) {
+      requestBody.backupStrategy = CollectionAdminParams.COPY_FILES_STRATEGY;
+    }
+    if 
(!CollectionAdminParams.INDEX_BACKUP_STRATEGIES.contains(requestBody.backupStrategy))
 {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Unknown index backup strategy " + requestBody.backupStrategy);
+    }
+
+    final ZkNodeProps remoteMessage = createRemoteMessage(collectionName, 
backupName, requestBody);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.BACKUP,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    final SolrJerseyResponse response =
+        objectMapper.convertValue(
+            remoteResponse.getResponse(), 
CreateCollectionBackupResponseBody.class);
+
+    return response;
+  }
+
+  public static ZkNodeProps createRemoteMessage(
+      String collectionName, String backupName, 
CreateCollectionBackupRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = requestBody.toMap(new 
HashMap<>());
+    remoteMessage.put(QUEUE_OPERATION, 
CollectionParams.CollectionAction.BACKUP.toLower());
+    remoteMessage.put(COLLECTION_PROP, collectionName);
+    remoteMessage.put(NAME, backupName);
+    if (!StringUtils.isBlank(requestBody.backupStrategy)) {
+      remoteMessage.put(INDEX_BACKUP_STRATEGY, 
remoteMessage.remove("backupStrategy"));
+    }
+    if (!StringUtils.isBlank(requestBody.snapshotName)) {
+      remoteMessage.put(COMMIT_NAME, remoteMessage.remove("snapshotName"));
+    }
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static CreateCollectionBackupRequestBody 
createRequestBodyFromV1Params(SolrParams params) {
+    var requestBody = new 
CreateCollectionBackupAPI.CreateCollectionBackupRequestBody();
+
+    requestBody.location = params.get(BACKUP_LOCATION);
+    requestBody.repository = params.get(BACKUP_REPOSITORY);
+    requestBody.followAliases = params.getBool(FOLLOW_ALIASES);
+    requestBody.backupStrategy = params.get(INDEX_BACKUP_STRATEGY);
+    requestBody.snapshotName = params.get(COMMIT_NAME);
+    requestBody.incremental = params.getBool(BACKUP_INCREMENTAL);
+    requestBody.maxNumBackupPoints = params.getInt(MAX_NUM_BACKUP_POINTS);
+    requestBody.async = params.get(ASYNC);
+
+    return requestBody;
+  }
+
+  public static SolrJerseyResponse invokeFromV1Params(
+      SolrQueryRequest req, SolrQueryResponse rsp, CoreContainer 
coreContainer) throws Exception {
+    req.getParams().required().check(NAME, COLLECTION_PROP);
+    final var collectionName = req.getParams().get(COLLECTION_PROP);
+    final var backupName = req.getParams().get(NAME);
+    final var requestBody = createRequestBodyFromV1Params(req.getParams());
+
+    final var createBackupApi = new CreateCollectionBackupAPI(coreContainer, 
req, rsp);
+    return createBackupApi.createCollectionBackup(collectionName, backupName, 
requestBody);
+  }
+
+  private String getLocation(BackupRepository repository, String location) 
throws IOException {
+    location = repository.getBackupLocation(location);
+    if (location != null) {
+      return location;
+    }
+
+    // Refresh the cluster property file to make sure the value set for 
location is the
+    // latest. Check if the location is specified in the cluster property.
+    location =
+        new ClusterProperties(coreContainer.getZkController().getZkClient())
+            .getClusterProperty(CoreAdminParams.BACKUP_LOCATION, null);
+    if (location != null) {
+      return location;
+    }
+
+    throw new SolrException(
+        SolrException.ErrorCode.BAD_REQUEST,
+        "'location' is not specified as a query"
+            + " parameter or as a default repository property or as a cluster 
property.");
+  }
+
+  public static class CreateCollectionBackupRequestBody implements 
JacksonReflectMapWriter {
+    @JsonProperty public String location;
+    @JsonProperty public String repository;
+    @JsonProperty public Boolean followAliases;
+    @JsonProperty public String backupStrategy;
+    @JsonProperty public String snapshotName;
+    @JsonProperty public Boolean incremental;
+    @JsonProperty public Integer maxNumBackupPoints;
+    @JsonProperty public String async;
+  }
+
+  public static class CreateCollectionBackupResponseBody
+      extends SubResponseAccumulatingJerseyResponse {
+    @JsonProperty("response")
+    public CollectionBackupData backupDataResponse;
+
+    @JsonProperty("deleted")
+    public List<BackupDeletionData> deleted;
+
+    @JsonProperty public String collection;
+  }
+
+  public static class CollectionBackupData implements JacksonReflectMapWriter {
+    @JsonProperty public String collection;
+    @JsonProperty public Integer numShards;
+    @JsonProperty public Integer backupId;
+    @JsonProperty public String indexVersion;
+    @JsonProperty public String startTime;
+    @JsonProperty public String endTime;
+    @JsonProperty public Integer indexFileCount;
+    @JsonProperty public Integer uploadedIndexFileCount;
+    @JsonProperty public Double indexSizeMB;
+
+    @JsonProperty("uploadedIndexFileMB")
+    public Double uploadedIndexSizeMB;
+
+    @JsonProperty public List<String> shardBackupIds;
+  }
+
+  public static class BackupDeletionData implements JacksonReflectMapWriter {
+    @JsonProperty public String startTime;
+    @JsonProperty public Integer backupId;
+    @JsonProperty public Long size;
+    @JsonProperty public Integer numFiles;
+  }
+}
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 00513d6ce82..77b4ca51105 100644
--- a/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
+++ b/solr/core/src/java/org/apache/solr/jersey/SolrJacksonMapper.java
@@ -32,8 +32,19 @@ import org.apache.solr.common.util.NamedList;
 @SuppressWarnings("rawtypes")
 @Provider
 public class SolrJacksonMapper implements ContextResolver<ObjectMapper> {
+
+  private static final ObjectMapper objectMapper = createObjectMapper();
+
   @Override
   public ObjectMapper getContext(Class<?> type) {
+    return objectMapper;
+  }
+
+  public static ObjectMapper getObjectMapper() {
+    return objectMapper;
+  }
+
+  private static ObjectMapper createObjectMapper() {
     final SimpleModule customTypeModule = new SimpleModule();
     customTypeModule.addSerializer(new NamedListSerializer(NamedList.class));
 
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 6c904312160..ab89ec0237e 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java
@@ -192,19 +192,15 @@ public class V2ApiIntegrationTest extends 
SolrCloudTestCase {
         "/collections/collection1/get",
         Utils.getObjectByPath(result, true, "/spec[0]/url/paths[0]"));
     String tempDir = createTempDir().toFile().getPath();
-    Map<String, Object> backupPayload = new HashMap<>();
     Map<String, Object> backupParams = new HashMap<>();
-    backupPayload.put("backup-collection", backupParams);
-    backupParams.put("name", "backup_test");
-    backupParams.put("collection", COLL_NAME);
     backupParams.put("location", tempDir);
     cluster
         .getJettySolrRunners()
         .forEach(j -> 
j.getCoreContainer().getAllowPaths().add(Paths.get(tempDir)));
     client.request(
-        new V2Request.Builder("/c")
+        new V2Request.Builder("/collections/" + COLL_NAME + 
"/backups/backup_test/versions")
             .withMethod(SolrRequest.METHOD.POST)
-            .withPayload(Utils.toJSONString(backupPayload))
+            .withPayload(Utils.toJSONString(backupParams))
             .build());
   }
 
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 d37d9146c21..439ec2b567f 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
@@ -175,36 +175,6 @@ public class V2CollectionsAPIMappingTest extends 
V2ApiMappingTest<CollectionsHan
             RoutedAlias.CREATE_COLLECTION_PREFIX + 
ZkStateReader.REPLICATION_FACTOR));
   }
 
-  @Test
-  public void testBackupAllProperties() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/collections",
-            "POST",
-            "{'backup-collection': {"
-                + "'name': 'backupName', "
-                + "'collection': 'collectionName', "
-                + "'location': '/some/location/uri', "
-                + "'repository': 'someRepository', "
-                + "'followAliases': true, "
-                + "'indexBackup': 'copy-files', "
-                + "'commitName': 'someSnapshotName', "
-                + "'incremental': true, "
-                + "'async': 'requestTrackingId' "
-                + "}}");
-
-    assertEquals(CollectionParams.CollectionAction.BACKUP.lowerName, 
v1Params.get(ACTION));
-    assertEquals("backupName", v1Params.get(CommonParams.NAME));
-    assertEquals("collectionName", 
v1Params.get(BackupManager.COLLECTION_NAME_PROP));
-    assertEquals("/some/location/uri", 
v1Params.get(CoreAdminParams.BACKUP_LOCATION));
-    assertEquals("someRepository", 
v1Params.get(CoreAdminParams.BACKUP_REPOSITORY));
-    
assertTrue(v1Params.getPrimitiveBool(CollectionAdminParams.FOLLOW_ALIASES));
-    assertEquals("copy-files", 
v1Params.get(CollectionAdminParams.INDEX_BACKUP_STRATEGY));
-    assertEquals("someSnapshotName", 
v1Params.get(CoreAdminParams.COMMIT_NAME));
-    assertTrue(v1Params.getPrimitiveBool(CoreAdminParams.BACKUP_INCREMENTAL));
-    assertEquals("requestTrackingId", v1Params.get(CommonAdminParams.ASYNC));
-  }
-
   @Test
   public void testRestoreAllProperties() throws Exception {
     final SolrParams v1Params =
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
new file mode 100644
index 00000000000..f8c51b1a8d0
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/V2CollectionBackupApiTest.java
@@ -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.handler.admin.api;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static 
org.apache.solr.common.params.CollectionAdminParams.COPY_FILES_STRATEGY;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.junit.Test;
+
+/** Unit tests for {@link CreateCollectionBackupAPI} */
+public class V2CollectionBackupApiTest extends SolrTestCaseJ4 {
+  @Test
+  public void testCreateRemoteMessageWithAllProperties() {
+    final var requestBody = new 
CreateCollectionBackupAPI.CreateCollectionBackupRequestBody();
+    requestBody.location = "/some/location";
+    requestBody.repository = "someRepoName";
+    requestBody.followAliases = true;
+    requestBody.backupStrategy = COPY_FILES_STRATEGY;
+    requestBody.snapshotName = "someSnapshotName";
+    requestBody.incremental = true;
+    requestBody.maxNumBackupPoints = 123;
+    requestBody.async = "someId";
+
+    var message =
+        CreateCollectionBackupAPI.createRemoteMessage(
+            "someCollectionName", "someBackupName", requestBody);
+    var messageProps = message.getProperties();
+
+    assertEquals(11, messageProps.size());
+    assertEquals("someCollectionName", messageProps.get("collection"));
+    assertEquals("/some/location", messageProps.get("location"));
+    assertEquals("someRepoName", messageProps.get("repository"));
+    assertEquals(true, messageProps.get("followAliases"));
+    assertEquals("copy-files", messageProps.get("indexBackup"));
+    assertEquals("someSnapshotName", messageProps.get("commitName"));
+    assertEquals(true, messageProps.get("incremental"));
+    assertEquals(123, messageProps.get("maxNumBackupPoints"));
+    assertEquals("someId", messageProps.get("async"));
+    assertEquals("backup", messageProps.get(QUEUE_OPERATION));
+    assertEquals("someBackupName", messageProps.get("name"));
+  }
+
+  @Test
+  public void testCreateRemoteMessageOmitsNullValues() {
+    final var requestBody = new 
CreateCollectionBackupAPI.CreateCollectionBackupRequestBody();
+    requestBody.location = "/some/location";
+
+    var message =
+        CreateCollectionBackupAPI.createRemoteMessage(
+            "someCollectionName", "someBackupName", requestBody);
+    var messageProps = message.getProperties();
+
+    assertEquals(4, messageProps.size());
+    assertEquals("someCollectionName", messageProps.get("collection"));
+    assertEquals("/some/location", messageProps.get("location"));
+    assertEquals("backup", messageProps.get(QUEUE_OPERATION));
+    assertEquals("someBackupName", messageProps.get("name"));
+  }
+
+  @Test
+  public void testCanCreateV2RequestBodyFromV1Params() {
+    final var params = new ModifiableSolrParams();
+    params.set("collection", "someCollectionName");
+    params.set("location", "/some/location");
+    params.set("repository", "someRepoName");
+    params.set("followAliases", "true");
+    params.set("indexBackup", COPY_FILES_STRATEGY);
+    params.set("commitName", "someSnapshotName");
+    params.set("incremental", "true");
+    params.set("maxNumBackupPoints", "123");
+    params.set("async", "someId");
+
+    final var requestBody = 
CreateCollectionBackupAPI.createRequestBodyFromV1Params(params);
+
+    assertEquals("/some/location", requestBody.location);
+    assertEquals("someRepoName", requestBody.repository);
+    assertEquals(Boolean.TRUE, requestBody.followAliases);
+    assertEquals("copy-files", requestBody.backupStrategy);
+    assertEquals("someSnapshotName", requestBody.snapshotName);
+    assertEquals(Boolean.TRUE, requestBody.incremental);
+    assertEquals(Integer.valueOf(123), requestBody.maxNumBackupPoints);
+    assertEquals("someId", requestBody.async);
+  }
+}
diff --git 
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc 
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
index aeb9c249b9d..3cfb11536aa 100644
--- 
a/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
+++ 
b/solr/solr-ref-guide/modules/deployment-guide/pages/collection-management.adoc
@@ -1542,7 +1542,7 @@ 
http://localhost:8983/solr/admin/collections?action=COLSTATUS&collection=getting
 [[backup]]
 == BACKUP: Backup Collection
 
-Backs up Solr collections and associated configurations to a shared filesystem 
- for example a Network File System.
+Backs up Solr collections and associated configurations to a "backup 
repository".
 
 [.dynamic-tabs]
 --
@@ -1561,24 +1561,18 @@ 
http://localhost:8983/solr/admin/collections?action=BACKUP&name=techproducts_bac
 ====
 [.tab-label]*V2 API*
 
-With the v2 API, the `backup-collection` command is provided as part of the 
JSON data that contains the required parameters:
-
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type: 
application/json' -d '
+curl -X POST 
http://localhost:8983/api/collections/techproducts/backups/techproducts_backup/versions
 -H 'Content-Type: application/json' -d '
   {
-    "backup-collection": {
-      "name": "techproducts_backup",
-      "collection": "techproducts",
-      "location": "file:///path/to/my/shared/drive"
-    }
+    "location": "file:///path/to/my/shared/drive"
   }
 '
 ----
 ====
 --
 
-The BACKUP command will backup Solr indexes and configurations for a specified 
collection.
+The BACKUP API will backup Solr indexes and configurations for a specified 
collection.
 The BACKUP command xref:backup-restore.adoc[takes one copy from each shard for 
the indexes].
 For configurations, it backs up the configset that was associated with the 
collection and metadata.
 
@@ -1607,6 +1601,7 @@ s|Required |Default: none
 |===
 +
 The name of the collection to be backed up.
+Provided as a query parameter for v1 requests, and as a path segment for v2 
requests.
 
 `name`::
 +
@@ -1616,6 +1611,7 @@ s|Required |Default: none
 |===
 +
 What to name the backup that is created.
+Provided as a query parameter for v1 requests, or as a path segment for v2 
requests.
 This is checked to make sure it doesn't already exist, and otherwise an error 
message is raised.
 
 `location`::
@@ -1677,6 +1673,25 @@ A boolean parameter allowing users to choose whether to 
create an incremental (`
 If unspecified, backups are done incrementally by default.
 Incremental backups are preferred in all known circumstances and snapshot 
backups are deprecated, so this parameter should only be used after much 
consideration.
 
+`indexBackup` (v1), `backupStrategy` (v2)::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: "copy-files"
+|===
++
+A string parameter allowing users to specify one of several different backup 
"strategies".
+Valid options are `copy-files` (which backs up both the collection configset 
and index data), and `none` (which will only backup the collection configset).
+
+`commitName` (v1), `snapshotName` (v2)::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+The name of a the collection "snapshot" to create a backup from.
+If not provided, Solr will create the backup from the current collection state 
(instead of a previous snapshotted state).
 
 [example.tab-pane#backup-response-incremental]
 ====
diff --git 
a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
 
b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
index 08070802011..d43fff4f38f 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/cloud/api/collections/AbstractIncrementalBackupTest.java
@@ -495,7 +495,7 @@ public abstract class AbstractIncrementalBackupTest extends 
SolrCloudTestCase {
       this.maxNumberOfBackupToKeep = maxNumberOfBackupToKeep;
     }
 
-    @SuppressWarnings("rawtypes")
+    @SuppressWarnings({"rawtypes", "unchecked"})
     private void backupThenWait() throws SolrServerException, IOException {
       CollectionAdminRequest.Backup backup =
           CollectionAdminRequest.backupCollection(getCollectionName(), 
backupName)
@@ -515,7 +515,7 @@ public abstract class AbstractIncrementalBackupTest extends 
SolrCloudTestCase {
       } else {
         CollectionAdminResponse rsp = backup.process(cluster.getSolrClient());
         assertEquals(0, rsp.getStatus());
-        NamedList resp = (NamedList) rsp.getResponse().get("response");
+        Map<String, Object> resp = (Map<String, Object>) 
rsp.getResponse().get("response");
         numBackup++;
         assertEquals(numBackup, resp.get("backupId"));
         ;


Reply via email to