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:12 UTC

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

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;
-}