You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/03/24 20:05:10 UTC

[solr] branch branch_9x updated (0f7b562c1e6 -> 45a3e4d121f)

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

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


    from 0f7b562c1e6 SOLR-15446: Fix three off-by-one ArrayList initialCapacity choices when calling ZooKeeper.multi API. (#156)
     new 0a36fd42fe2 SOLR-16393: Migrate alias-deletion to JAX-RS (#1452)
     new 45a3e4d121f SOLR-16393 Migrate aliasprop CRUD APIs to JAX-RS (#1459)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 solr/CHANGES.txt                                   |   9 +
 .../org/apache/solr/handler/CollectionsAPI.java    |  30 ---
 .../solr/handler/admin/CollectionsHandler.java     |  50 ++--
 .../handler/admin/api/AddReplicaPropertyAPI.java   |   3 +
 .../solr/handler/admin/api/AliasPropertyAPI.java   | 264 +++++++++++++++++++++
 .../handler/admin/api/CollectionPropertyAPI.java   |   4 +
 ...eleteCollectionAPI.java => DeleteAliasAPI.java} |  47 ++--
 .../AsyncJerseyResponse.java}                      |  10 +-
 .../SubResponseAccumulatingJerseyResponse.java     |   5 +-
 .../apache/solr/cloud/AliasIntegrationTest.java    | 105 ++++++--
 .../solr/handler/admin/TestCollectionAPIs.java     |   7 -
 .../handler/admin/V2CollectionsAPIMappingTest.java |  32 ---
 .../solr/handler/admin/api/DeleteAliasAPITest.java |  45 ++++
 .../deployment-guide/pages/alias-management.adoc   | 111 +++++++--
 .../solrj/request/CollectionAdminRequest.java      |   6 +-
 .../solrj/request/beans/DeleteAliasPayload.java    |  27 ---
 .../request/beans/SetAliasPropertyPayload.java     |  30 ---
 17 files changed, 553 insertions(+), 232 deletions(-)
 create mode 100644 solr/core/src/java/org/apache/solr/handler/admin/api/AliasPropertyAPI.java
 copy solr/core/src/java/org/apache/solr/handler/admin/api/{DeleteCollectionAPI.java => DeleteAliasAPI.java} (65%)
 copy solr/core/src/java/org/apache/solr/{cluster/placement/DeleteShardsRequest.java => jersey/AsyncJerseyResponse.java} (79%)
 create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java
 delete mode 100644 solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteAliasPayload.java
 delete mode 100644 solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetAliasPropertyPayload.java


[solr] 01/02: SOLR-16393: Migrate alias-deletion to JAX-RS (#1452)

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 0a36fd42fe2f5cafb321d495733bd86121992b0e
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Wed Mar 15 10:22:29 2023 -0400

    SOLR-16393: Migrate alias-deletion to JAX-RS (#1452)
    
    This commit makes various cosmetic improvements to Solr's v2 delete
    alias API, to bring it more into line with the more REST-ful v2
    design.  In the process it also converts the API to the JAX-RS
    framework.
    
    As of this commit, the delete-alias API is now
        - DELETE /api/aliases/aliasName
---
 solr/CHANGES.txt                                   |  3 +
 .../org/apache/solr/handler/CollectionsAPI.java    | 12 ---
 .../solr/handler/admin/CollectionsHandler.java     | 13 ++-
 .../solr/handler/admin/api/DeleteAliasAPI.java     | 92 ++++++++++++++++++++++
 .../apache/solr/jersey/AsyncJerseyResponse.java}   | 12 ++-
 .../SubResponseAccumulatingJerseyResponse.java     |  5 +-
 .../solr/handler/admin/TestCollectionAPIs.java     |  7 --
 .../handler/admin/V2CollectionsAPIMappingTest.java | 13 ---
 .../solr/handler/admin/api/DeleteAliasAPITest.java | 45 +++++++++++
 .../deployment-guide/pages/alias-management.adoc   | 10 +--
 10 files changed, 160 insertions(+), 52 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index e394d109e8d..37c59339387 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -19,6 +19,9 @@ Improvements
 
 * SOLR-16638: Fix Http2SolrClient's exception message when serverBaseUrl is null (Alex Deparvu via Kevin Risden)
 
+* SOLR-16393: The path of the v2 "delete alias" API has been tweaked slightly to be more intuitive, and is now available at
+  `DELETE /api/aliases/aliasName`. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 
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 a2b76ced5b8..1e3aeac45ee 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -39,7 +39,6 @@ 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.DeleteAliasPayload;
 import org.apache.solr.client.solrj.request.beans.RestoreCollectionPayload;
 import org.apache.solr.client.solrj.request.beans.SetAliasPropertyPayload;
 import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
@@ -55,7 +54,6 @@ public class CollectionsAPI {
   public static final String V2_RESTORE_CMD = "restore-collection";
   public static final String V2_CREATE_ALIAS_CMD = "create-alias";
   public static final String V2_SET_ALIAS_PROP_CMD = "set-alias-property";
-  public static final String V2_DELETE_ALIAS_CMD = "delete-alias";
 
   private final CollectionsHandler collectionsHandler;
 
@@ -142,16 +140,6 @@ public class CollectionsAPI {
           wrapParams(obj.getRequest(), v1Params), obj.getResponse());
     }
 
-    @Command(name = V2_DELETE_ALIAS_CMD)
-    public void deleteAlias(PayloadObj<DeleteAliasPayload> obj) throws Exception {
-      final DeleteAliasPayload v2Body = obj.get();
-      final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-      v1Params.put(ACTION, CollectionAction.DELETEALIAS.toLower());
-
-      collectionsHandler.handleRequestBody(
-          wrapParams(obj.getRequest(), v1Params), obj.getResponse());
-    }
-
     @Command(name = V2_CREATE_COLLECTION_CMD)
     public void create(PayloadObj<CreatePayload> obj) throws Exception {
       final CreatePayload v2Body = obj.get();
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 de9f3dd9556..89a66df0611 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
@@ -209,6 +209,7 @@ 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.CreateShardAPI;
+import org.apache.solr.handler.admin.api.DeleteAliasAPI;
 import org.apache.solr.handler.admin.api.DeleteCollectionAPI;
 import org.apache.solr.handler.admin.api.DeleteNodeAPI;
 import org.apache.solr.handler.admin.api.DeleteReplicaAPI;
@@ -855,7 +856,16 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
           return result;
         }),
 
-    DELETEALIAS_OP(DELETEALIAS, (req, rsp, h) -> copy(req.getParams().required(), null, NAME)),
+    DELETEALIAS_OP(
+        DELETEALIAS,
+        (req, rsp, h) -> {
+          final DeleteAliasAPI deleteAliasAPI = new DeleteAliasAPI(h.coreContainer, req, rsp);
+          final SolrJerseyResponse response =
+              deleteAliasAPI.deleteAlias(
+                  req.getParams().required().get(NAME), req.getParams().get(ASYNC));
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
+          return null;
+        }),
 
     /**
      * Change properties for an alias (use CREATEALIAS_OP to change the actual value of the alias)
@@ -2076,6 +2086,7 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
     return List.of(
         AddReplicaPropertyAPI.class,
+        DeleteAliasAPI.class,
         DeleteCollectionAPI.class,
         DeleteReplicaPropertyAPI.class,
         ListCollectionsAPI.class,
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAliasAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAliasAPI.java
new file mode 100644
index 00000000000..4b9a4480507
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/DeleteAliasAPI.java
@@ -0,0 +1,92 @@
+/*
+ * 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.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+
+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.AsyncJerseyResponse;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+@Path("/aliases/{aliasName}")
+public class DeleteAliasAPI extends AdminAPIBase {
+  @Inject
+  public DeleteAliasAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @DELETE
+  @Produces({"application/json", "application/xml", BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse deleteAlias(
+      @PathParam("aliasName") String aliasName, @QueryParam("async") String asyncId)
+      throws Exception {
+    final AsyncJerseyResponse response = instantiateJerseyResponse(AsyncJerseyResponse.class);
+    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+
+    final ZkNodeProps remoteMessage = createRemoteMessage(aliasName, asyncId);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.DELETEALIAS,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    if (asyncId != null) {
+      response.requestId = asyncId;
+    }
+
+    return response;
+  }
+
+  public static ZkNodeProps createRemoteMessage(String aliasName, String asyncId) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.DELETEALIAS.toLower());
+    remoteMessage.put(NAME, aliasName);
+    if (asyncId != null) remoteMessage.put(ASYNC, asyncId);
+
+    return new ZkNodeProps(remoteMessage);
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteAliasPayload.java b/solr/core/src/java/org/apache/solr/jersey/AsyncJerseyResponse.java
similarity index 71%
rename from solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteAliasPayload.java
rename to solr/core/src/java/org/apache/solr/jersey/AsyncJerseyResponse.java
index 4ccd0601a66..af08d227172 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/DeleteAliasPayload.java
+++ b/solr/core/src/java/org/apache/solr/jersey/AsyncJerseyResponse.java
@@ -14,14 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.client.solrj.request.beans;
 
-import org.apache.solr.common.annotation.JsonProperty;
-import org.apache.solr.common.util.ReflectMapWriter;
+package org.apache.solr.jersey;
 
-public class DeleteAliasPayload implements ReflectMapWriter {
-  @JsonProperty(required = true)
-  public String name;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
-  @JsonProperty public String async;
+public class AsyncJerseyResponse extends SolrJerseyResponse {
+  @JsonProperty("requestid")
+  public String requestId;
 }
diff --git a/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java b/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
index ab476771d75..6f02e9b6b1c 100644
--- a/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
+++ b/solr/core/src/java/org/apache/solr/jersey/SubResponseAccumulatingJerseyResponse.java
@@ -27,10 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
  * 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;
+public class SubResponseAccumulatingJerseyResponse extends AsyncJerseyResponse {
 
   // 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
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 326066239ad..35847701431 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
@@ -129,13 +129,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
         "{create-alias:{name: aliasName , collections:[c1,c2] }}",
         "{operation : createalias, name: aliasName, collections:\"c1,c2\" }");
 
-    compareOutput(
-        apiBag,
-        "/collections",
-        POST,
-        "{delete-alias:{ name: aliasName}}",
-        "{operation : deletealias, name: aliasName}");
-
     compareOutput(
         apiBag, "/collections/collName", POST, "{reload:{}}", "{name:collName, operation :reload}");
 
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 2949fd38a67..ae099f24cb7 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,19 +175,6 @@ public class V2CollectionsAPIMappingTest extends V2ApiMappingTest<CollectionsHan
             RoutedAlias.CREATE_COLLECTION_PREFIX + ZkStateReader.REPLICATION_FACTOR));
   }
 
-  @Test
-  public void testDeleteAliasAllProperties() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/collections",
-            "POST",
-            "{'delete-alias': {" + "'name': 'aliasName', " + "'async': 'requestTrackingId'" + "}}");
-
-    assertEquals(CollectionParams.CollectionAction.DELETEALIAS.lowerName, v1Params.get(ACTION));
-    assertEquals("aliasName", v1Params.get(CommonParams.NAME));
-    assertEquals("requestTrackingId", v1Params.get(CommonAdminParams.ASYNC));
-  }
-
   @Test
   public void testSetAliasAllProperties() throws Exception {
     final SolrParams v1Params =
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java
new file mode 100644
index 00000000000..2dcaf14d183
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/api/DeleteAliasAPITest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CoreAdminParams.NAME;
+
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.Test;
+
+/** Unit tests for {@link DeleteAliasAPI}. */
+public class DeleteAliasAPITest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testConstructsValidRemoteMessage() {
+    Map<String, Object> props =
+        DeleteAliasAPI.createRemoteMessage("aliasName", null).getProperties();
+    assertEquals(2, props.size());
+    assertEquals("deletealias", props.get(QUEUE_OPERATION));
+    assertEquals("aliasName", props.get(NAME));
+
+    props = DeleteAliasAPI.createRemoteMessage("aliasName", "asyncId").getProperties();
+    assertEquals(3, props.size());
+    assertEquals("deletealias", props.get(QUEUE_OPERATION));
+    assertEquals("aliasName", props.get(NAME));
+    assertEquals("asyncId", props.get(ASYNC));
+  }
+}
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc
index 0c96b84dd9c..6c9e810ca4a 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc
@@ -745,13 +745,7 @@ http://localhost:8983/solr/admin/collections?action=DELETEALIAS&name=testalias
 
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type: application/json' -d '
-{
-  "delete-alias":{
-    "name":"testalias"
-  }
-}
-'
+curl -X DELETE http://localhost:8983/api/aliases/testalias
 ----
 ====
 --
@@ -766,7 +760,7 @@ curl -X POST http://localhost:8983/api/collections -H 'Content-Type: application
 s|Required |Default: none
 |===
 +
-The name of the alias to delete.
+The name of the alias to delete.  Specified in the path of v2 requests, and as an explicit request parameter for v1 requests.
 
 `async`::
 +


[solr] 02/02: SOLR-16393 Migrate aliasprop CRUD APIs to JAX-RS (#1459)

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 45a3e4d121f905e4de3d5bdc2e3ccb9df8bb1630
Author: Alex <st...@users.noreply.github.com>
AuthorDate: Thu Mar 16 11:49:21 2023 -0700

    SOLR-16393 Migrate aliasprop CRUD APIs to JAX-RS (#1459)
    
    This commit makes various cosmetic improvements to Solr's v2 alias
    property CRUD APIs, to bring them more into line with the REST-ful v2
    design.  In the process it also converts the APIs to the JAX-RS
    framework.  Modified v2 APIs include:
        - PUT /api/aliases/aliasName/properties (bulk update)
        - PUT /api/aliases/aliasName/properties/propName (single update)
        - GET /api/aliases/aliasName/properties (new API: list props)
        - GET /api/aliases/aliasName/properties/propName (get single prop)
        - DELETE /api/aliases/aliasName/properties/propName (delete prop)
---
 solr/CHANGES.txt                                   |   6 +
 .../org/apache/solr/handler/CollectionsAPI.java    |  18 --
 .../solr/handler/admin/CollectionsHandler.java     |  37 +--
 .../handler/admin/api/AddReplicaPropertyAPI.java   |   3 +
 .../solr/handler/admin/api/AliasPropertyAPI.java   | 264 +++++++++++++++++++++
 .../handler/admin/api/CollectionPropertyAPI.java   |   4 +
 .../apache/solr/cloud/AliasIntegrationTest.java    | 105 ++++++--
 .../handler/admin/V2CollectionsAPIMappingTest.java |  19 --
 .../deployment-guide/pages/alias-management.adoc   | 101 +++++++-
 .../solrj/request/CollectionAdminRequest.java      |   6 +-
 .../request/beans/SetAliasPropertyPayload.java     |  30 ---
 11 files changed, 471 insertions(+), 122 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 37c59339387..a35508f1e44 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -22,6 +22,12 @@ Improvements
 * SOLR-16393: The path of the v2 "delete alias" API has been tweaked slightly to be more intuitive, and is now available at
   `DELETE /api/aliases/aliasName`. (Jason Gerlowski)
 
+* SOLR-16393: Solr's v2 "aliasprop" CRUD APIs have been tweaked slightly to be more intuitive.  Alias property modification
+  is now available at `PUT /api/aliases/aliasName/properties` (for bulk modification) and `PUT /api/aliases/aliasName/properties/propName`
+  (for single property updates).  Additionally new APIs have been added for listing properties (`GET /api/aliases/aliasName/properties`),
+  fetching single property values (`GET /api/aliases/aliasName/properties/propName`), and property deletion
+  (`DELETE /api/aliases/aliasName/properties/propName`). (Alex Deparvu via Jason Gerlowski)
+
 Optimizations
 ---------------------
 
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 1e3aeac45ee..79af6a65d31 100644
--- a/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/CollectionsAPI.java
@@ -40,7 +40,6 @@ 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;
-import org.apache.solr.client.solrj.request.beans.SetAliasPropertyPayload;
 import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
 import org.apache.solr.common.params.CollectionAdminParams;
 import org.apache.solr.common.params.CollectionParams.CollectionAction;
@@ -53,7 +52,6 @@ public class CollectionsAPI {
   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";
-  public static final String V2_SET_ALIAS_PROP_CMD = "set-alias-property";
 
   private final CollectionsHandler collectionsHandler;
 
@@ -124,22 +122,6 @@ public class CollectionsAPI {
           wrapParams(obj.getRequest(), v1Params), obj.getResponse());
     }
 
-    @Command(name = V2_SET_ALIAS_PROP_CMD)
-    @SuppressWarnings("unchecked")
-    public void setAliasProperty(PayloadObj<SetAliasPropertyPayload> obj) throws Exception {
-      final SetAliasPropertyPayload v2Body = obj.get();
-      final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
-
-      v1Params.put(ACTION, CollectionAction.ALIASPROP.toLower());
-      // Flatten "properties" map into individual prefixed params
-      final Map<String, Object> propertiesMap =
-          (Map<String, Object>) v1Params.remove(V2ApiConstants.PROPERTIES_KEY);
-      flattenMapWithPrefix(propertiesMap, v1Params, PROPERTY_PREFIX);
-
-      collectionsHandler.handleRequestBody(
-          wrapParams(obj.getRequest(), v1Params), obj.getResponse());
-    }
-
     @Command(name = V2_CREATE_COLLECTION_CMD)
     public void create(PayloadObj<CreatePayload> obj) throws Exception {
       final CreatePayload v2Body = obj.get();
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 89a66df0611..51778caad64 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
@@ -205,6 +205,7 @@ import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.handler.admin.api.AddReplicaAPI;
 import org.apache.solr.handler.admin.api.AddReplicaPropertyAPI;
 import org.apache.solr.handler.admin.api.AdminAPIBase;
+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;
@@ -873,11 +874,17 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
     ALIASPROP_OP(
         ALIASPROP,
         (req, rsp, h) -> {
-          Map<String, Object> params = copy(req.getParams().required(), null, NAME);
-
-          // Note: success/no-op in the event of no properties supplied is intentional. Keeps code
-          // simple and one less case for api-callers to check for.
-          return convertPrefixToMap(req.getParams(), params, "property");
+          String name = req.getParams().required().get(NAME);
+          Map<String, Object> properties = collectToMap(req.getParams(), "property");
+          AliasPropertyAPI.UpdateAliasPropertiesRequestBody requestBody =
+              new AliasPropertyAPI.UpdateAliasPropertiesRequestBody();
+          requestBody.properties = properties;
+          requestBody.async = req.getParams().get(ASYNC);
+          final AliasPropertyAPI aliasPropertyAPI = new AliasPropertyAPI(h.coreContainer, req, rsp);
+          final SolrJerseyResponse getAliasesResponse =
+              aliasPropertyAPI.updateAliasProperties(name, requestBody);
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, getAliasesResponse);
+          return null;
         }),
 
     /** List the aliases and associated properties. */
@@ -1840,28 +1847,21 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         });
 
     /**
-     * Places all prefixed properties in the sink map (or a new map) using the prefix as the key and
-     * a map of all prefixed properties as the value. The sub-map keys have the prefix removed.
+     * Collects all prefixed properties in a new map. The resulting keys have the prefix removed.
      *
      * @param params The solr params from which to extract prefixed properties.
-     * @param sink The map to add the properties too.
      * @param prefix The prefix to identify properties to be extracted
-     * @return The sink map, or a new map if the sink map was null
+     * @return a map with collected properties
      */
-    private static Map<String, Object> convertPrefixToMap(
-        SolrParams params, Map<String, Object> sink, String prefix) {
-      Map<String, Object> result = new LinkedHashMap<>();
+    private static Map<String, Object> collectToMap(SolrParams params, String prefix) {
+      Map<String, Object> sink = new LinkedHashMap<>();
       Iterator<String> iter = params.getParameterNamesIterator();
       while (iter.hasNext()) {
         String param = iter.next();
         if (param.startsWith(prefix)) {
-          result.put(param.substring(prefix.length() + 1), params.get(param));
+          sink.put(param.substring(prefix.length() + 1), params.get(param));
         }
       }
-      if (sink == null) {
-        sink = new LinkedHashMap<>();
-      }
-      sink.put(prefix, result);
       return sink;
     }
 
@@ -2093,7 +2093,8 @@ public class CollectionsHandler extends RequestHandlerBase implements Permission
         ReplaceNodeAPI.class,
         CollectionPropertyAPI.class,
         DeleteNodeAPI.class,
-        ListAliasesAPI.class);
+        ListAliasesAPI.class,
+        AliasPropertyAPI.class);
   }
 
   @Override
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java
index 5bb8e5395df..30936638ce5 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AddReplicaPropertyAPI.java
@@ -92,6 +92,9 @@ public class AddReplicaPropertyAPI extends AdminAPIBase {
               required = true)
           AddReplicaPropertyRequestBody requestBody)
       throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
+    }
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
     final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/AliasPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/AliasPropertyAPI.java
new file mode 100644
index 00000000000..02d2f7024c5
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/AliasPropertyAPI.java
@@ -0,0 +1,264 @@
+/*
+ * 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.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
+import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.parameters.RequestBody;
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.Aliases;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.cloud.ZkStateReader;
+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.JacksonReflectMapWriter;
+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 APIs for managing and inspecting properties for collection aliases */
+@Path("/aliases/{aliasName}/properties")
+public class AliasPropertyAPI extends AdminAPIBase {
+
+  @Inject
+  public AliasPropertyAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @GET
+  @PermissionName(COLL_READ_PERM)
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Operation(
+      summary = "Get properties for a collection alias.",
+      tags = {"aliases"})
+  public GetAllAliasPropertiesResponse getAllAliasProperties(
+      @Parameter(description = "Alias Name") @PathParam("aliasName") String aliasName)
+      throws Exception {
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+
+    final GetAllAliasPropertiesResponse response =
+        instantiateJerseyResponse(GetAllAliasPropertiesResponse.class);
+    final Aliases aliases = readAliasesFromZk();
+    if (aliases != null) {
+      response.properties = aliases.getCollectionAliasProperties(aliasName);
+    } else {
+      throw new SolrException(SolrException.ErrorCode.NOT_FOUND, aliasName + " not found");
+    }
+
+    return response;
+  }
+
+  @GET
+  @Path("/{propName}")
+  @PermissionName(COLL_READ_PERM)
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Operation(
+      summary = "Get a specific property for a collection alias.",
+      tags = {"aliases"})
+  public GetAliasPropertyResponse getAliasProperty(
+      @Parameter(description = "Alias Name") @PathParam("aliasName") String aliasName,
+      @Parameter(description = "Property Name") @PathParam("propName") String propName)
+      throws Exception {
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+
+    final GetAliasPropertyResponse response =
+        instantiateJerseyResponse(GetAliasPropertyResponse.class);
+    final Aliases aliases = readAliasesFromZk();
+    if (aliases != null) {
+      String value = aliases.getCollectionAliasProperties(aliasName).get(propName);
+      if (value != null) {
+        response.value = value;
+      } else {
+        throw new SolrException(SolrException.ErrorCode.NOT_FOUND, propName + " not found");
+      }
+    }
+
+    return response;
+  }
+
+  private Aliases readAliasesFromZk() throws Exception {
+    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    final ZkStateReader zkStateReader = coreContainer.getZkController().getZkStateReader();
+    // Make sure we have the latest alias info, since a user has explicitly invoked an alias API
+    zkStateReader.getAliasesManager().update();
+    return zkStateReader.getAliases();
+  }
+
+  @PUT
+  @PermissionName(COLL_EDIT_PERM)
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Operation(
+      summary = "Update properties for a collection alias.",
+      tags = {"aliases"})
+  public SolrJerseyResponse updateAliasProperties(
+      @Parameter(description = "Alias Name") @PathParam("aliasName") String aliasName,
+      @RequestBody(description = "Properties that need to be updated", required = true)
+          UpdateAliasPropertiesRequestBody requestBody)
+      throws Exception {
+
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
+    }
+
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+
+    SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    modifyAliasProperties(aliasName, requestBody.properties, requestBody.async);
+    return response;
+  }
+
+  @PUT
+  @Path("/{propName}")
+  @PermissionName(COLL_EDIT_PERM)
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Operation(
+      summary = "Update a specific property for a collection alias.",
+      tags = {"aliases"})
+  public SolrJerseyResponse createOrUpdateAliasProperty(
+      @Parameter(description = "Alias Name") @PathParam("aliasName") String aliasName,
+      @Parameter(description = "Property Name") @PathParam("propName") String propName,
+      @RequestBody(description = "Property value that needs to be updated", required = true)
+          UpdateAliasPropertyRequestBody requestBody)
+      throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
+    }
+
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+
+    SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    modifyAliasProperty(aliasName, propName, requestBody.value);
+    return response;
+  }
+
+  @DELETE
+  @Path("/{propName}")
+  @PermissionName(COLL_EDIT_PERM)
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, BINARY_CONTENT_TYPE_V2})
+  @Operation(
+      summary = "Delete a specific property for a collection alias.",
+      tags = {"aliases"})
+  public SolrJerseyResponse deleteAliasProperty(
+      @Parameter(description = "Alias Name") @PathParam("aliasName") String aliasName,
+      @Parameter(description = "Property Name") @PathParam("propName") String propName)
+      throws Exception {
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+
+    SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    modifyAliasProperty(aliasName, propName, null);
+    return response;
+  }
+
+  private void modifyAliasProperty(String alias, String proertyName, Object value)
+      throws Exception {
+    Map<String, Object> props = new HashMap<>();
+    // value can be null
+    props.put(proertyName, value);
+    modifyAliasProperties(alias, props, null);
+  }
+
+  /**
+   * @param alias alias
+   */
+  private void modifyAliasProperties(String alias, Map<String, Object> properties, String async)
+      throws Exception {
+    // Note: success/no-op in the event of no properties supplied is intentional. Keeps code
+    // simple and one less case for api-callers to check for.
+    final CoreContainer coreContainer = fetchAndValidateZooKeeperAwareCoreContainer();
+    final ZkNodeProps remoteMessage = createRemoteMessage(alias, properties, async);
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.ALIASPROP,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    disableResponseCaching();
+  }
+
+  private static final String PROPERTIES = "property";
+
+  public ZkNodeProps createRemoteMessage(
+      String alias, Map<String, Object> properties, String async) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, CollectionParams.CollectionAction.ALIASPROP.toLower());
+    remoteMessage.put(NAME, alias);
+    remoteMessage.put(PROPERTIES, properties);
+    if (async != null) {
+      remoteMessage.put(ASYNC, async);
+    }
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static class UpdateAliasPropertiesRequestBody implements JacksonReflectMapWriter {
+
+    @Schema(description = "Properties and values to be updated on alias.")
+    @JsonProperty(value = "properties", required = true)
+    public Map<String, Object> properties;
+
+    @Schema(description = "Request ID to track this action which will be processed asynchronously.")
+    @JsonProperty("async")
+    public String async;
+  }
+
+  public static class UpdateAliasPropertyRequestBody implements JacksonReflectMapWriter {
+    @JsonProperty(required = true)
+    public Object value;
+  }
+
+  public static class GetAllAliasPropertiesResponse extends SolrJerseyResponse {
+    @JsonProperty("properties")
+    @Schema(description = "Properties and values associated with alias.")
+    public Map<String, String> properties;
+  }
+
+  public static class GetAliasPropertyResponse extends SolrJerseyResponse {
+    @JsonProperty("value")
+    @Schema(description = "Property value.")
+    public String value;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java
index 72d3c1e8145..5302571dcad 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CollectionPropertyAPI.java
@@ -28,6 +28,7 @@ import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.CollectionProperties;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.jersey.JacksonReflectMapWriter;
@@ -60,6 +61,9 @@ public class CollectionPropertyAPI extends AdminAPIBase {
       @PathParam("propName") String propName,
       UpdateCollectionPropertyRequestBody requestBody)
       throws Exception {
+    if (requestBody == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Missing required request body");
+    }
     final SolrJerseyResponse response = instantiateJerseyResponse(SolrJerseyResponse.class);
     recordCollectionForLogAndTracing(collName, solrQueryRequest);
     modifyCollectionProperty(collName, propName, requestBody.value);
diff --git a/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java b/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java
index 3c57c892c8d..0e7be0e6544 100644
--- a/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java
@@ -20,13 +20,15 @@ import static org.apache.solr.common.cloud.ZkStateReader.ALIASES;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
@@ -238,28 +240,51 @@ public class AliasIntegrationTest extends SolrCloudTestCase {
   public void testModifyPropertiesV2() throws Exception {
     final String aliasName = getSaferTestName();
     ZkStateReader zkStateReader = createColectionsAndAlias(aliasName);
-    final String baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString();
-    // TODO fix Solr test infra so that this /____v2/ becomes /api/
-    HttpPost post = new HttpPost(baseUrl + "/____v2/c");
-    post.setEntity(
+    final String baseUrl =
+        cluster.getRandomJetty(random()).getBaseUrl().toString().replace("/solr", "");
+    String aliasApi = String.format(Locale.ENGLISH, "/api/aliases/%s/properties", aliasName);
+
+    HttpPut withoutBody = new HttpPut(baseUrl + aliasApi);
+    assertEquals(400, httpClient.execute(withoutBody).getStatusLine().getStatusCode());
+
+    HttpPut update = new HttpPut(baseUrl + aliasApi);
+    update.setEntity(
         new StringEntity(
             "{\n"
-                + "\"set-alias-property\" : {\n"
-                + "  \"name\": \""
-                + aliasName
-                + "\",\n"
-                + "  \"properties\" : {\n"
-                + "    \"foo\": \"baz\",\n"
-                + "    \"bar\": \"bam\"\n"
+                + "    \"properties\":\n"
+                + "    {\n"
+                + "        \"foo\": \"baz\",\n"
+                + "        \"bar\": \"bam\"\n"
+                + "    }\n"
+                + "}",
+            ContentType.APPLICATION_JSON));
+    assertSuccess(update);
+    checkFooAndBarMeta(aliasName, zkStateReader, "baz", "bam");
+
+    String aliasPropertyApi =
+        String.format(Locale.ENGLISH, "/api/aliases/%s/properties/%s", aliasName, "foo");
+    HttpPut updateByProperty = new HttpPut(baseUrl + aliasPropertyApi);
+    updateByProperty.setEntity(
+        new StringEntity("{ \"value\": \"zab\" }", ContentType.APPLICATION_JSON));
+    assertSuccess(updateByProperty);
+    checkFooAndBarMeta(aliasName, zkStateReader, "zab", "bam");
+
+    HttpDelete deleteByProperty = new HttpDelete(baseUrl + aliasPropertyApi);
+    assertSuccess(deleteByProperty);
+    checkFooAndBarMeta(aliasName, zkStateReader, null, "bam");
+
+    HttpPut deleteByEmptyValue = new HttpPut(baseUrl + aliasApi);
+    deleteByEmptyValue.setEntity(
+        new StringEntity(
+            "{\n"
+                + "    \"properties\":\n"
+                + "    {\n"
+                + "        \"bar\": \"\"\n"
                 + "    }\n"
-                +
-                // TODO should we use "NOW=" param?  Won't work with v2 and is kinda a hack any way
-                // since intended for distrib
-                "  }\n"
                 + "}",
             ContentType.APPLICATION_JSON));
-    assertSuccess(post);
-    checkFooAndBarMeta(aliasName, zkStateReader);
+    assertSuccess(deleteByEmptyValue);
+    checkFooAndBarMeta(aliasName, zkStateReader, null, null);
   }
 
   @Test
@@ -278,7 +303,19 @@ public class AliasIntegrationTest extends SolrCloudTestCase {
                 + "&property.foo=baz"
                 + "&property.bar=bam");
     assertSuccess(get);
-    checkFooAndBarMeta(aliasName, zkStateReader);
+    checkFooAndBarMeta(aliasName, zkStateReader, "baz", "bam");
+
+    HttpGet remove =
+        new HttpGet(
+            baseUrl
+                + "/admin/collections?action=ALIASPROP"
+                + "&wt=xml"
+                + "&name="
+                + aliasName
+                + "&property.foo="
+                + "&property.bar=bar");
+    assertSuccess(remove);
+    checkFooAndBarMeta(aliasName, zkStateReader, null, "bar");
   }
 
   @Test
@@ -291,20 +328,24 @@ public class AliasIntegrationTest extends SolrCloudTestCase {
     setAliasProperty.addProperty("foo", "baz");
     setAliasProperty.addProperty("bar", "bam");
     setAliasProperty.process(cluster.getSolrClient());
-    checkFooAndBarMeta(aliasName, zkStateReader);
+    checkFooAndBarMeta(aliasName, zkStateReader, "baz", "bam");
 
     // now verify we can delete
     setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);
     setAliasProperty.addProperty("foo", "");
     setAliasProperty.process(cluster.getSolrClient());
+    checkFooAndBarMeta(aliasName, zkStateReader, null, "bam");
+
     setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);
     setAliasProperty.addProperty("bar", null);
     setAliasProperty.process(cluster.getSolrClient());
-    setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);
+    checkFooAndBarMeta(aliasName, zkStateReader, null, null);
 
+    setAliasProperty = CollectionAdminRequest.setAliasProperty(aliasName);
     // whitespace value
     setAliasProperty.addProperty("foo", " ");
     setAliasProperty.process(cluster.getSolrClient());
+    checkFooAndBarMeta(aliasName, zkStateReader, " ", null);
   }
 
   @Test
@@ -413,14 +454,26 @@ public class AliasIntegrationTest extends SolrCloudTestCase {
     return -1;
   }
 
-  private void checkFooAndBarMeta(String aliasName, ZkStateReader zkStateReader) throws Exception {
+  private void checkFooAndBarMeta(
+      String aliasName, ZkStateReader zkStateReader, String fooValue, String barValue)
+      throws Exception {
     zkStateReader.aliasesManager.update(); // ensure our view is up-to-date
     Map<String, String> meta = zkStateReader.getAliases().getCollectionAliasProperties(aliasName);
     assertNotNull(meta);
-    assertTrue(meta.containsKey("foo"));
-    assertEquals("baz", meta.get("foo"));
-    assertTrue(meta.containsKey("bar"));
-    assertEquals("bam", meta.get("bar"));
+
+    if (fooValue != null) {
+      assertTrue(meta.containsKey("foo"));
+      assertEquals(fooValue, meta.get("foo"));
+    } else {
+      assertFalse(meta.toString(), meta.containsKey("foo"));
+    }
+
+    if (barValue != null) {
+      assertTrue(meta.containsKey("bar"));
+      assertEquals(barValue, meta.get("bar"));
+    } else {
+      assertFalse(meta.toString(), meta.containsKey("bar"));
+    }
   }
 
   private ZkStateReader createColectionsAndAlias(String aliasName)
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 ae099f24cb7..d37d9146c21 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,25 +175,6 @@ public class V2CollectionsAPIMappingTest extends V2ApiMappingTest<CollectionsHan
             RoutedAlias.CREATE_COLLECTION_PREFIX + ZkStateReader.REPLICATION_FACTOR));
   }
 
-  @Test
-  public void testSetAliasAllProperties() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/collections",
-            "POST",
-            "{'set-alias-property': {"
-                + "'name': 'aliasName', "
-                + "'async': 'requestTrackingId', "
-                + "'properties': {'foo':'bar', 'foo2':'bar2'}"
-                + "}}");
-
-    assertEquals(CollectionParams.CollectionAction.ALIASPROP.lowerName, v1Params.get(ACTION));
-    assertEquals("aliasName", v1Params.get(CommonParams.NAME));
-    assertEquals("requestTrackingId", v1Params.get(CommonAdminParams.ASYNC));
-    assertEquals("bar", v1Params.get("property.foo"));
-    assertEquals("bar2", v1Params.get("property.foo2"));
-  }
-
   @Test
   public void testBackupAllProperties() throws Exception {
     final SolrParams v1Params =
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc
index 6c9e810ca4a..f1cf9242813 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/alias-management.adoc
@@ -640,7 +640,7 @@ curl -X GET http://localhost:8983/api/aliases/testalias2
 ----
 
 [[aliasprop]]
-== ALIASPROP: Modify Alias Properties for a Collection
+== ALIASPROP: Modify Alias Properties
 
 The `ALIASPROP` action modifies the properties (metadata) on an alias.
 If a key is set with a value that is empty it will be removed.
@@ -653,7 +653,7 @@ If a key is set with a value that is empty it will be removed.
 
 [source,bash]
 ----
-http://localhost:8983/admin/collections?action=ALIASPROP&name=techproducts_alias&property.foo=bar
+curl -X POST 'http://localhost:8983/admin/collections?action=ALIASPROP&name=techproducts_alias&property.foo=bar'
 ----
 ====
 
@@ -663,17 +663,39 @@ http://localhost:8983/admin/collections?action=ALIASPROP&name=techproducts_alias
 
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type: application/json' -d '
+curl -X PUT http://localhost:8983/api/aliases/techproducts_alias/properties -H 'Content-Type: application/json' -d '
 {
-  "set-alias-property":{
-    "name":"techproducts_alias",
-    "properties": {"foo":"bar"}
-  }
-}
-'
+  "properties": {"foo":"bar"}
+}'
+----
+
+====
+
+[example.tab-pane#v2aliasplevelprop]
+====
+[.tab-label]*V2 API* Update via property level api
+
+[source,bash]
+----
+curl -X PUT http://localhost:8983/api/aliases/techproducts_alias/properties/foo -H 'Content-Type: application/json' -d '
+{
+  "value": "baz"
+}'
 ----
 
 ====
+
+[example.tab-pane#v2deleteplevelprop]
+====
+[.tab-label]*V2 API* Delete via property level api
+
+[source,bash]
+----
+curl -X DELETE http://localhost:8983/api/aliases/techproducts_alias/properties/foo -H 'Content-Type: application/json'
+----
+
+====
+
 --
 
 
@@ -722,7 +744,66 @@ Request ID to track this action which will be xref:configuration-guide:collectio
 === ALIASPROP Response
 
 The output will simply be a responseHeader with details of the time it took to process the request.
-To confirm the creation of the property or properties, you can look in the Solr Admin UI, under the Cloud section and find the `aliases.json` file or use the LISTALIASES api command.
+Alias property creation can be confirmed using the "List Alias Properties" APIs described below, or by inspecting the `aliases.json` in the "Cloud" section of the Solr Admin UI.
+
+[[aliaspropread]]
+== Listing Alias Properties
+
+Retrieves the metadata properties associated with a specified alias.
+Solr's v2 API supports either listing out these properties in bulk or accessing them individually by name, as necessary.
+
+
+[.dynamic-tabs]
+--
+[example.tab-pane#v2listallprops]
+====
+[.tab-label]*V2 API* Get all properties on an alias
+
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/aliases/techproducts_alias/properties
+----
+
+*Output*
+
+[source,json]
+----
+{
+  "responseHeader": {
+    "status": 0,
+    "QTime": 1
+  },
+  "properties": {
+    "foo": "bar"
+  }
+}
+----
+====
+
+[example.tab-pane#v2listsingleprop]
+====
+[.tab-label]*V2 API* Get single property value on an alias
+
+[source,bash]
+----
+curl -X GET http://localhost:8983/api/aliases/techproducts_alias/properties/foo
+----
+
+*Output*
+
+[source,json]
+----
+{
+  "responseHeader": {
+    "status": 0,
+    "QTime": 1
+  },
+  "value": "bar"
+}
+----
+====
+--
+
 
 [[deletealias]]
 == DELETEALIAS: Delete a Collection Alias
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
index 0a41b2c782a..fad9264b8d6 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionAdminRequest.java
@@ -1893,7 +1893,11 @@ public abstract class CollectionAdminRequest<T extends CollectionAdminResponse>
     }
 
     public SetAliasProperty addProperty(String key, String value) {
-      properties.put(key, value);
+      if (value == null) {
+        properties.put(key, "");
+      } else {
+        properties.put(key, value);
+      }
       return this;
     }
 
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetAliasPropertyPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetAliasPropertyPayload.java
deleted file mode 100644
index 93133cd2051..00000000000
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/SetAliasPropertyPayload.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.solrj.request.beans;
-
-import java.util.Map;
-import org.apache.solr.common.annotation.JsonProperty;
-import org.apache.solr.common.util.ReflectMapWriter;
-
-public class SetAliasPropertyPayload implements ReflectMapWriter {
-  @JsonProperty(required = true)
-  public String name;
-
-  @JsonProperty public String async;
-
-  @JsonProperty public Map<String, Object> properties;
-}