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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1a2ab763ec8 SOLR-16393: Tweak v2 "create alias" API to be more 
REST-ful (#1590)
1a2ab763ec8 is described below

commit 1a2ab763ec8a564f352344fdf6eeb9871f414b7f
Author: Jason Gerlowski <[email protected]>
AuthorDate: Wed Apr 26 11:02:17 2023 -0400

    SOLR-16393: Tweak v2 "create alias" API to be more REST-ful (#1590)
    
    This commit makes various cosmetic improvements to Solr's v2
    create alias API, to bring it more into line with the more REST-ful
    direction we're targeting for our v2 API.  The v2 API is now available
    at: `POST /api/aliases`.  The request body has also been restructured
    slightly: the top-level "create-alias" command property has been
    removed, the router "name" field has been renamed to "type", and
    routers now are always defined in a list.
    
    This commit also migrates the API to JAX-RS.
---
 solr/CHANGES.txt                                   |   5 +
 .../solr/cloud/api/collections/RoutedAlias.java    |   6 +-
 .../cloud/api/collections/TimeRoutedAlias.java     |   4 +-
 .../solr/handler/admin/CollectionsHandler.java     | 168 +--------
 .../solr/handler/admin/api/CreateAliasAPI.java     | 416 +++++++++++++++++++--
 .../handler/admin/api/CreateCollectionAPI.java     |  13 +-
 .../apache/solr/cloud/AliasIntegrationTest.java    |   2 +-
 .../apache/solr/cloud/CreateRoutedAliasTest.java   |  74 ++--
 .../solr/handler/admin/TestCollectionAPIs.java     |  10 -
 .../handler/admin/V2CollectionsAPIMappingTest.java |  68 ----
 .../solr/handler/admin/api/CreateAliasAPITest.java | 348 +++++++++++++++++
 .../deployment-guide/pages/alias-management.adoc   | 104 +++---
 .../solrj/request/beans/CreateAliasPayload.java    |  63 ----
 13 files changed, 849 insertions(+), 432 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 79456936332..dcdd1a7fd08 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -100,6 +100,11 @@ Improvements
 
 * SOLR-15493: Throw an error message if the feature store doesn't exist.  
(Ilaria Petreti via Alessandro Benedetti)
 
+* SOLR-16393: The v2 "create alias" API has been tweaked to be more intuitive. 
 The format of the request body has changed
+  slightly: the "create-alias" command specifier has been removed, the "name" 
field for individual routers has been renamed
+  to "type", and the routers themselves are now always grouped into a list. 
Additionally the v2 API has moved to the new path
+  `POST /api/aliases`. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 
diff --git 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java
index 93abb90faf4..d41eed3611d 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java
@@ -62,7 +62,11 @@ public abstract class RoutedAlias {
   public static final Set<String> MINIMAL_REQUIRED_PARAMS = 
Set.of(ROUTER_TYPE_NAME, ROUTER_FIELD);
 
   public static final String ROUTED_ALIAS_NAME_CORE_PROP = "routedAliasName"; 
// core prop
-  private static final String DIMENSIONAL = "Dimensional[";
+  public static final String DIMENSIONAL = "Dimensional[";
+
+  public static final String TIME = "time";
+
+  public static final String CATEGORY = "category";
 
   // This class is created once per request and the overseer methods prevent 
duplicate create
   // requests from creating extra copies via locking on the alias name. All we 
need to track here is
diff --git 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java
index ec7395bb81f..c833217d6b1 100644
--- 
a/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java
+++ 
b/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java
@@ -248,13 +248,13 @@ public class TimeRoutedAlias extends RoutedAlias {
     return aliasName + TYPE.getSeparatorPrefix() + nextCollName;
   }
 
-  private Instant parseStringAsInstant(String str, TimeZone zone) {
+  public static Instant parseStringAsInstant(String str, TimeZone zone) {
     Instant start = DateMathParser.parseMath(new Date(), str, 
zone).toInstant();
     checkMillis(start);
     return start;
   }
 
-  private void checkMillis(Instant date) {
+  private static void checkMillis(Instant date) {
     if (!date.truncatedTo(ChronoUnit.SECONDS).equals(date)) {
       throw new SolrException(
           BAD_REQUEST,
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 4268a95420f..aa9dd746529 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
@@ -30,7 +30,6 @@ import static 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY
 import static 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.ONLY_IF_DOWN;
 import static 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
 import static 
org.apache.solr.cloud.api.collections.CollectionHandlingUtils.SHARD_UNIQUE;
-import static 
org.apache.solr.cloud.api.collections.RoutedAlias.CREATE_COLLECTION_PREFIX;
 import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
 import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
 import static org.apache.solr.common.cloud.ZkStateReader.NRT_REPLICAS;
@@ -124,14 +123,12 @@ import static 
org.apache.solr.common.params.ShardParams._ROUTE_;
 import static org.apache.solr.common.util.StrUtils.formatString;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -161,11 +158,9 @@ import 
org.apache.solr.cloud.ZkController.NotInClusterStateException;
 import org.apache.solr.cloud.ZkShardTerms;
 import 
org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
 import org.apache.solr.cloud.api.collections.ReindexCollectionCmd;
-import org.apache.solr.cloud.api.collections.RoutedAlias;
 import org.apache.solr.cloud.overseer.SliceMutator;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.Aliases;
 import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.ClusterState;
 import org.apache.solr.common.cloud.DocCollection;
@@ -175,7 +170,6 @@ import org.apache.solr.common.cloud.Replica;
 import org.apache.solr.common.cloud.Replica.State;
 import org.apache.solr.common.cloud.Slice;
 import org.apache.solr.common.cloud.SolrZkClient;
-import org.apache.solr.common.cloud.ZkCmdExecutor;
 import org.apache.solr.common.cloud.ZkCoreNodeProps;
 import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.cloud.ZkStateReader;
@@ -243,7 +237,6 @@ import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
 import org.apache.solr.util.tracing.TraceUtils;
-import org.apache.zookeeper.CreateMode;
 import org.apache.zookeeper.KeeperException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -299,16 +292,6 @@ public class CollectionsHandler extends RequestHandlerBase 
implements Permission
     return this.coreContainer;
   }
 
-  protected void copyFromClusterProp(Map<String, Object> props, String prop) 
throws IOException {
-    if (props.get(prop) != null) return; // if it's already specified , return
-    Object defVal =
-        new 
ClusterProperties(coreContainer.getZkController().getZkStateReader().getZkClient())
-            .getClusterProperty(
-                List.of(CollectionAdminParams.DEFAULTS, 
CollectionAdminParams.COLLECTION, prop),
-                null);
-    if (defVal != null) props.put(prop, String.valueOf(defVal));
-  }
-
   @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
     // Make sure the cores is enabled
@@ -539,42 +522,6 @@ public class CollectionsHandler extends RequestHandlerBase 
implements Permission
     return Category.ADMIN;
   }
 
-  private static void createSysConfigSet(CoreContainer coreContainer)
-      throws KeeperException, InterruptedException {
-    SolrZkClient zk = 
coreContainer.getZkController().getZkStateReader().getZkClient();
-    ZkCmdExecutor cmdExecutor = new ZkCmdExecutor(zk.getZkClientTimeout());
-    cmdExecutor.ensureExists(ZkStateReader.CONFIGS_ZKNODE, zk);
-    cmdExecutor.ensureExists(
-        ZkStateReader.CONFIGS_ZKNODE + "/" + 
CollectionAdminParams.SYSTEM_COLL, zk);
-
-    try {
-      String path =
-          ZkStateReader.CONFIGS_ZKNODE + "/" + 
CollectionAdminParams.SYSTEM_COLL + "/schema.xml";
-      byte[] data;
-      try (InputStream inputStream =
-          
CollectionsHandler.class.getResourceAsStream("/SystemCollectionSchema.xml")) {
-        assert inputStream != null;
-        data = inputStream.readAllBytes();
-      }
-      assert data != null && data.length > 0;
-      cmdExecutor.ensureExists(path, data, CreateMode.PERSISTENT, zk);
-      path =
-          ZkStateReader.CONFIGS_ZKNODE
-              + "/"
-              + CollectionAdminParams.SYSTEM_COLL
-              + "/solrconfig.xml";
-      try (InputStream inputStream =
-          
CollectionsHandler.class.getResourceAsStream("/SystemCollectionSolrConfig.xml"))
 {
-        assert inputStream != null;
-        data = inputStream.readAllBytes();
-      }
-      assert data != null && data.length > 0;
-      cmdExecutor.ensureExists(path, data, CreateMode.PERSISTENT, zk);
-    } catch (IOException e) {
-      throw new SolrException(ErrorCode.SERVER_ERROR, e);
-    }
-  }
-
   private static void addStatusToResponse(
       NamedList<Object> results, RequestStatusState state, String msg) {
     SimpleOrderedMap<String> status = new SimpleOrderedMap<>();
@@ -716,107 +663,12 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
     CREATEALIAS_OP(
         CREATEALIAS,
         (req, rsp, h) -> {
-          String alias = req.getParams().get(NAME);
-          SolrIdentifierValidator.validateAliasName(alias);
-          String collections = req.getParams().get("collections");
-          RoutedAlias routedAlias = null;
-          Exception ex = null;
-          HashMap<String, Object> possiblyModifiedParams = new HashMap<>();
-          try {
-            // note that RA specific validation occurs here.
-            req.getParams().toMap(possiblyModifiedParams);
-            @SuppressWarnings({"unchecked", "rawtypes"})
-            // This is awful because RoutedAlias lies about what types it wants
-            Map<String, String> temp = (Map<String, String>) (Map) 
possiblyModifiedParams;
-            routedAlias = RoutedAlias.fromProps(alias, temp);
-          } catch (SolrException e) {
-            // we'll throw this later if we are in fact creating a routed 
alias.
-            ex = e;
-          }
-          ModifiableSolrParams finalParams = new ModifiableSolrParams();
-          for (Map.Entry<String, Object> entry : 
possiblyModifiedParams.entrySet()) {
-            if (entry.getValue().getClass().isArray()) {
-              // v2 api hits this case
-              for (Object o : (Object[]) entry.getValue()) {
-                finalParams.add(entry.getKey(), o.toString());
-              }
-            } else {
-              finalParams.add(entry.getKey(), entry.getValue().toString());
-            }
-          }
-
-          if (collections != null) {
-            if (routedAlias != null) {
-              throw new SolrException(
-                  BAD_REQUEST, "Collections cannot be specified when creating 
a routed alias.");
-            } else {
-              //////////////////////////////////////
-              // Regular alias creation indicated //
-              //////////////////////////////////////
-              return copy(finalParams.required(), null, NAME, "collections");
-            }
-          } else {
-            if (routedAlias != null) {
-              CoreContainer coreContainer1 = h.getCoreContainer();
-              Aliases aliases = coreContainer1.getAliases();
-              String aliasName = routedAlias.getAliasName();
-              if (aliases.hasAlias(aliasName) && 
!aliases.isRoutedAlias(aliasName)) {
-                throw new SolrException(
-                    BAD_REQUEST,
-                    "Cannot add routing parameters to existing non-routed 
Alias: " + aliasName);
-              }
-            }
-          }
-
-          /////////////////////////////////////////////////
-          // We are creating a routed alias from here on //
-          /////////////////////////////////////////////////
-
-          // If our prior creation attempt had issues expose them now.
-          if (ex != null) {
-            throw ex;
-          }
-
-          // Now filter out just the parameters we care about from the request
-          assert routedAlias != null;
-          Map<String, Object> result = copy(finalParams, null, 
routedAlias.getRequiredParams());
-          copy(finalParams, result, routedAlias.getOptionalParams());
-
-          ModifiableSolrParams createCollParams = new ModifiableSolrParams(); 
// without prefix
-
-          // add to result params that start with "create-collection.".
-          //   Additionally, save these without the prefix to createCollParams
-          for (Map.Entry<String, String[]> entry : finalParams) {
-            final String p = entry.getKey();
-            if (p.startsWith(CREATE_COLLECTION_PREFIX)) {
-              // This is what SolrParams#getAll(Map, Collection)} does
-              final String[] v = entry.getValue();
-              if (v.length == 1) {
-                result.put(p, v[0]);
-              } else {
-                result.put(p, v);
-              }
-              
createCollParams.set(p.substring(CREATE_COLLECTION_PREFIX.length()), v);
-            }
-          }
-
-          // Verify that the create-collection prefix'ed params appear to be 
valid.
-          if (createCollParams.get(NAME) != null) {
-            throw new SolrException(
-                BAD_REQUEST,
-                "routed aliases calculate names for their "
-                    + "dependent collections, you cannot specify the name.");
-          }
-          if (createCollParams.get(COLL_CONF) == null) {
-            throw new SolrException(
-                SolrException.ErrorCode.BAD_REQUEST, "We require an explicit " 
+ COLL_CONF);
-          }
-          final var createRequestBody =
-              
CreateCollectionAPI.CreateCollectionRequestBody.fromV1Params(createCollParams, 
false);
-          createRequestBody.name = "TMP_name_TMP_name_TMP"; // just to pass 
validation
-          createRequestBody.validate();
-
-          return result;
+          final CreateAliasAPI.CreateAliasRequestBody reqBody =
+              CreateAliasAPI.createFromSolrParams(req.getParams());
+          final SolrJerseyResponse response =
+              new CreateAliasAPI(h.coreContainer, req, 
rsp).createAlias(reqBody);
+          V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, response);
+          return null;
         }),
 
     DELETEALIAS_OP(
@@ -1929,12 +1781,6 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
     }
   }
 
-  private static void verifyShardsParam(String shardsParam) {
-    for (String shard : shardsParam.split(",")) {
-      SolrIdentifierValidator.validateShardName(shard);
-    }
-  }
-
   interface CollectionOp {
     Map<String, Object> execute(SolrQueryRequest req, SolrQueryResponse rsp, 
CollectionsHandler h)
         throws Exception;
@@ -1949,6 +1795,7 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
     return List.of(
         AddReplicaPropertyAPI.class,
+        CreateAliasAPI.class,
         CreateCollectionAPI.class,
         CreateCollectionBackupAPI.class,
         DeleteAliasAPI.class,
@@ -1969,7 +1816,6 @@ public class CollectionsHandler extends 
RequestHandlerBase implements Permission
   @Override
   public Collection<Api> getApis() {
     final List<Api> apis = new ArrayList<>();
-    apis.addAll(AnnotatedApi.getApis(new CreateAliasAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new RestoreCollectionAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new SplitShardAPI(this)));
     apis.addAll(AnnotatedApi.getApis(new CreateShardAPI(this)));
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAliasAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAliasAPI.java
index 41ebdbcefda..95f191c485a 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAliasAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAliasAPI.java
@@ -17,63 +17,401 @@
 
 package org.apache.solr.handler.admin.api;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
+import static 
org.apache.solr.client.solrj.impl.BinaryResponseParser.BINARY_CONTENT_TYPE_V2;
+import static 
org.apache.solr.client.solrj.request.beans.V2ApiConstants.COLLECTIONS;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.cloud.api.collections.RoutedAlias.CATEGORY;
 import static 
org.apache.solr.cloud.api.collections.RoutedAlias.CREATE_COLLECTION_PREFIX;
+import static 
org.apache.solr.cloud.api.collections.RoutedAlias.ROUTER_TYPE_NAME;
+import static org.apache.solr.cloud.api.collections.RoutedAlias.TIME;
+import static 
org.apache.solr.cloud.api.collections.TimeRoutedAlias.ROUTER_MAX_FUTURE;
+import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
 import static 
org.apache.solr.common.params.CollectionAdminParams.ROUTER_PREFIX;
-import static org.apache.solr.common.params.CommonParams.ACTION;
-import static org.apache.solr.handler.ClusterAPI.wrapParams;
-import static 
org.apache.solr.handler.admin.api.CreateCollectionAPI.CreateCollectionRequestBody.convertV2CreateCollectionMapToV1ParamMap;
-import static org.apache.solr.handler.api.V2ApiUtils.flattenMapWithPrefix;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.common.params.CommonParams.START;
+import static 
org.apache.solr.handler.admin.CollectionsHandler.DEFAULT_COLLECTION_OP_TIMEOUT;
 import static 
org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
-import org.apache.solr.api.Command;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.api.PayloadObj;
-import org.apache.solr.client.solrj.request.beans.CreateAliasPayload;
-import org.apache.solr.client.solrj.request.beans.V2ApiConstants;
+import javax.inject.Inject;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.solr.client.solrj.RoutedAliasTypes;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.solrj.util.SolrIdentifierValidator;
+import org.apache.solr.cloud.api.collections.RoutedAlias;
+import org.apache.solr.cloud.api.collections.TimeRoutedAlias;
+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.params.CollectionParams;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.CollectionUtil;
+import org.apache.solr.common.util.StrUtils;
+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.jersey.SubResponseAccumulatingJerseyResponse;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.TimeZoneUtils;
 
-@EndPoint(
-    path = {"/aliases"},
-    method = POST,
-    permission = COLL_EDIT_PERM)
-public class CreateAliasAPI {
+/**
+ * V2 API for creating an alias
+ *
+ * <p>This API is analogous to the v1 /admin/collections?action=CREATEALIAS 
command.
+ */
+@Path("/aliases")
+public class CreateAliasAPI extends AdminAPIBase {
+  @Inject
+  public CreateAliasAPI(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @POST
+  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, 
BINARY_CONTENT_TYPE_V2})
+  @PermissionName(COLL_EDIT_PERM)
+  public SolrJerseyResponse createAlias(CreateAliasRequestBody requestBody) 
throws Exception {
+    final SubResponseAccumulatingJerseyResponse response =
+        instantiateJerseyResponse(SubResponseAccumulatingJerseyResponse.class);
+    recordCollectionForLogAndTracing(null, solrQueryRequest);
+
+    if (requestBody == null) {
+      throw new SolrException(BAD_REQUEST, "Request body is required but 
missing");
+    }
+    requestBody.validate();
+
+    ZkNodeProps remoteMessage;
+    // Validation ensures that the request has either collections or a router 
but not both.
+    if (CollectionUtil.isNotEmpty(requestBody.collections)) {
+      remoteMessage = createRemoteMessageForTraditionalAlias(requestBody);
+    } else { // Creating a routed alias
+      assert CollectionUtil.isNotEmpty(requestBody.routers);
+      final Aliases aliases = coreContainer.getAliases();
+      if (aliases.hasAlias(requestBody.name) && 
!aliases.isRoutedAlias(requestBody.name)) {
+        throw new SolrException(
+            BAD_REQUEST,
+            "Cannot add routing parameters to existing non-routed Alias: " + 
requestBody.name);
+      }
+
+      remoteMessage = createRemoteMessageForRoutedAlias(requestBody);
+    }
+
+    final SolrResponse remoteResponse =
+        CollectionsHandler.submitCollectionApiCommand(
+            coreContainer,
+            coreContainer.getDistributedCollectionCommandRunner(),
+            remoteMessage,
+            CollectionParams.CollectionAction.CREATEALIAS,
+            DEFAULT_COLLECTION_OP_TIMEOUT);
+    if (remoteResponse.getException() != null) {
+      throw remoteResponse.getException();
+    }
+
+    if (requestBody.async != null) {
+      response.requestId = requestBody.async;
+    }
+    return response;
+  }
+
+  public static ZkNodeProps createRemoteMessageForTraditionalAlias(
+      CreateAliasRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+
+    remoteMessage.put(QUEUE_OPERATION, 
CollectionParams.CollectionAction.CREATEALIAS.toLower());
+    remoteMessage.put(NAME, requestBody.name);
+    remoteMessage.put("collections", String.join(",", 
requestBody.collections));
+    remoteMessage.put(ASYNC, requestBody.async);
+
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static ZkNodeProps 
createRemoteMessageForRoutedAlias(CreateAliasRequestBody requestBody) {
+    final Map<String, Object> remoteMessage = new HashMap<>();
+    remoteMessage.put(QUEUE_OPERATION, 
CollectionParams.CollectionAction.CREATEALIAS.toLower());
+    remoteMessage.put(NAME, requestBody.name);
+    if (StrUtils.isNotBlank(requestBody.async)) remoteMessage.put(ASYNC, 
requestBody.async);
+
+    if (requestBody.routers.size() > 1) { // Multi-dimensional alias
+      for (int i = 0; i < requestBody.routers.size(); i++) {
+        requestBody.routers.get(i).addRemoteMessageProperties(remoteMessage, 
"router." + i + ".");
+      }
+    } else if (requestBody.routers.size() == 1) { // Single dimensional alias
+      requestBody.routers.get(0).addRemoteMessageProperties(remoteMessage, 
"router.");
+    }
+
+    if (requestBody.collCreationParameters != null) {
+      requestBody.collCreationParameters.addToRemoteMessageWithPrefix(
+          remoteMessage, "create-collection.");
+    }
+    return new ZkNodeProps(remoteMessage);
+  }
+
+  public static CreateAliasRequestBody createFromSolrParams(SolrParams params) 
{
+    final CreateAliasRequestBody createBody = new CreateAliasRequestBody();
+    createBody.name = params.required().get(NAME);
+
+    final String collections = params.get(COLLECTIONS);
+    createBody.collections =
+        StrUtils.isNullOrEmpty(collections) ? new ArrayList<>() : 
StrUtils.split(collections, ',');
+    createBody.async = params.get(ASYNC);
+
+    // Handle routed-alias properties
+    final String typeStr = params.get(ROUTER_TYPE_NAME);
+    if (typeStr == null) {
+      return createBody; // non-routed aliases are being created
+    }
+
+    createBody.routers = new ArrayList<>();
+    if (typeStr.startsWith(RoutedAlias.DIMENSIONAL)) {
+      final String commaSeparatedDimensions =
+          typeStr.substring(RoutedAlias.DIMENSIONAL.length(), typeStr.length() 
- 1);
+      final String[] dimensions = commaSeparatedDimensions.split(",");
+      if (dimensions.length > 2) {
+        throw new SolrException(
+            BAD_REQUEST,
+            "More than 2 dimensions is not supported yet. "
+                + "Please monitor SOLR-13628 for progress");
+      }
+
+      for (int i = 0; i < dimensions.length; i++) {
+        createBody.routers.add(
+            createFromSolrParams(dimensions[i], params, ROUTER_PREFIX + i + 
"."));
+      }
+    } else {
+      createBody.routers.add(createFromSolrParams(typeStr, params, 
ROUTER_PREFIX));
+    }
+
+    final SolrParams createCollectionParams =
+        getHierarchicalParametersByPrefix(params, CREATE_COLLECTION_PREFIX);
+    createBody.collCreationParameters =
+        
CreateCollectionAPI.CreateCollectionRequestBody.fromV1Params(createCollectionParams,
 false);
+
+    return createBody;
+  }
+
+  public static RoutedAliasProperties createFromSolrParams(
+      String type, SolrParams params, String propertyPrefix) {
+    final String typeLower = type.toLowerCase(Locale.ROOT);
+    if (typeLower.startsWith(TIME)) {
+      return TimeRoutedAliasProperties.createFromSolrParams(params, 
propertyPrefix);
+    } else if (typeLower.startsWith(CATEGORY)) {
+      return CategoryRoutedAliasProperties.createFromSolrParams(params, 
propertyPrefix);
+    } else {
+      throw new SolrException(
+          BAD_REQUEST,
+          "Router name: "
+              + type
+              + " is not in supported types, "
+              + Arrays.asList(RoutedAliasTypes.values()));
+    }
+  }
 
-  public static final String V2_CREATE_ALIAS_CMD = "create-alias";
+  public static class CreateAliasRequestBody implements 
JacksonReflectMapWriter {
+    @JsonProperty(required = true)
+    public String name;
 
-  private final CollectionsHandler collectionsHandler;
+    @JsonProperty("collections")
+    public List<String> collections;
 
-  public CreateAliasAPI(CollectionsHandler collectionsHandler) {
-    this.collectionsHandler = collectionsHandler;
+    @JsonProperty(ASYNC)
+    public String async;
+
+    @JsonProperty("routers")
+    public List<RoutedAliasProperties> routers;
+
+    @JsonProperty("create-collection")
+    public CreateCollectionAPI.CreateCollectionRequestBody 
collCreationParameters;
+
+    public void validate() {
+      SolrIdentifierValidator.validateAliasName(name);
+
+      if (CollectionUtil.isEmpty(collections) && 
CollectionUtil.isEmpty(routers)) {
+        throw new SolrException(
+            BAD_REQUEST,
+            "Alias creation requires either a list of either collections (for 
creating a traditional alias) or routers (for creating a routed alias)");
+      }
+
+      if (CollectionUtil.isNotEmpty(routers)) {
+        routers.forEach(r -> r.validate());
+        if (CollectionUtil.isNotEmpty(collections)) {
+          throw new SolrException(
+              BAD_REQUEST, "Collections cannot be specified when creating a 
routed alias.");
+        }
+
+        final CreateCollectionAPI.CreateCollectionRequestBody 
createCollReqBody =
+            collCreationParameters;
+        if (createCollReqBody != null) {
+          if (createCollReqBody.name != null) {
+            throw new SolrException(
+                BAD_REQUEST,
+                "routed aliases calculate names for their "
+                    + "dependent collections, you cannot specify the name.");
+          }
+          if (createCollReqBody.config == null) {
+            throw new SolrException(
+                SolrException.ErrorCode.BAD_REQUEST,
+                "Routed alias creation requires a configset name to use for 
any collections created by the alias.");
+          }
+        }
+      }
+    }
   }
 
-  @Command(name = V2_CREATE_ALIAS_CMD)
-  @SuppressWarnings("unchecked")
-  public void createAlias(PayloadObj<CreateAliasPayload> obj) throws Exception 
{
-    final CreateAliasPayload v2Body = obj.get();
-    final Map<String, Object> v1Params = v2Body.toMap(new HashMap<>());
+  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = 
JsonTypeInfo.As.PROPERTY, property = "type")
+  @JsonSubTypes({
+    @JsonSubTypes.Type(value = TimeRoutedAliasProperties.class, name = "time"),
+    @JsonSubTypes.Type(value = CategoryRoutedAliasProperties.class, name = 
"category")
+  })
+  public abstract static class RoutedAliasProperties implements 
JacksonReflectMapWriter {
+    @JsonProperty(required = true)
+    public String field;
+
+    public abstract void validate();
 
-    v1Params.put(ACTION, 
CollectionParams.CollectionAction.CREATEALIAS.toLower());
-    if (v2Body.collections != null && !v2Body.collections.isEmpty()) {
-      final String collectionsStr = String.join(",", v2Body.collections);
-      v1Params.remove(V2ApiConstants.COLLECTIONS);
-      v1Params.put(V2ApiConstants.COLLECTIONS, collectionsStr);
+    public abstract void addRemoteMessageProperties(
+        Map<String, Object> remoteMessage, String prefix);
+
+    protected void ensureRequiredFieldPresent(Object val, String name) {
+      if (val == null) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST, "Missing required parameter: 
" + name);
+      }
     }
-    if (v2Body.router != null) {
-      Map<String, Object> routerProperties =
-          (Map<String, Object>) v1Params.remove(V2ApiConstants.ROUTER_KEY);
-      flattenMapWithPrefix(routerProperties, v1Params, ROUTER_PREFIX);
+  }
+
+  public static class TimeRoutedAliasProperties extends RoutedAliasProperties {
+    // Expected to be a date/time in ISO format, or 'NOW'
+    @JsonProperty(required = true)
+    public String start;
+
+    // TODO Change this to 'timezone' or something less abbreviated
+    @JsonProperty("tz")
+    public String tz;
+
+    @JsonProperty(required = true)
+    public String interval;
+
+    @JsonProperty("maxFutureMs")
+    public Long maxFutureMs;
+
+    @JsonProperty("preemptiveCreateMath")
+    public String preemptiveCreateMath;
+
+    @JsonProperty("autoDeleteAge")
+    public String autoDeleteAge;
+
+    @Override
+    public void validate() {
+      ensureRequiredFieldPresent(field, "'field' on time routed alias");
+      ensureRequiredFieldPresent(start, "'start' on time routed alias");
+      ensureRequiredFieldPresent(interval, "'interval' on time routed alias");
+
+      // Ensures that provided 'start' and optional 'tz' are of the right 
format.
+      TimeRoutedAlias.parseStringAsInstant(start, 
TimeZoneUtils.parseTimezone(tz));
+
+      // maxFutureMs must be > 0 if provided
+      if (maxFutureMs != null && maxFutureMs < 0) {
+        throw new SolrException(BAD_REQUEST, ROUTER_MAX_FUTURE + " must be >= 
0");
+      }
     }
-    if (v2Body.createCollectionParams != null && 
!v2Body.createCollectionParams.isEmpty()) {
-      final Map<String, Object> createCollectionMap =
-          (Map<String, Object>) 
v1Params.remove(V2ApiConstants.CREATE_COLLECTION_KEY);
-      convertV2CreateCollectionMapToV1ParamMap(createCollectionMap);
-      flattenMapWithPrefix(createCollectionMap, v1Params, 
CREATE_COLLECTION_PREFIX);
+
+    @Override
+    public void addRemoteMessageProperties(Map<String, Object> remoteMessage, 
String prefix) {
+      remoteMessage.put(prefix + CoreAdminParams.NAME, TIME);
+      remoteMessage.put(prefix + "field", field);
+      remoteMessage.put(prefix + "start", start);
+      remoteMessage.put(prefix + "interval", interval);
+
+      if (tz != null) remoteMessage.put(prefix + "tz", tz);
+      if (maxFutureMs != null) remoteMessage.put(prefix + "maxFutureMs", 
maxFutureMs);
+      if (preemptiveCreateMath != null)
+        remoteMessage.put(prefix + "preemptiveCreateMath", 
preemptiveCreateMath);
+      if (autoDeleteAge != null) remoteMessage.put(prefix + "autoDeleteAge", 
autoDeleteAge);
+    }
+
+    public static TimeRoutedAliasProperties createFromSolrParams(
+        SolrParams params, String propertyPrefix) {
+      final TimeRoutedAliasProperties timeRoutedProperties = new 
TimeRoutedAliasProperties();
+      timeRoutedProperties.field = params.required().get(propertyPrefix + 
"field");
+      timeRoutedProperties.start = params.required().get(propertyPrefix + 
START);
+      timeRoutedProperties.interval = params.required().get(propertyPrefix + 
"interval");
+
+      timeRoutedProperties.tz = params.get(propertyPrefix + "tz");
+      timeRoutedProperties.maxFutureMs = params.getLong(propertyPrefix + 
"maxFutureMs");
+      timeRoutedProperties.preemptiveCreateMath =
+          params.get(propertyPrefix + "preemptiveCreateMath");
+      timeRoutedProperties.autoDeleteAge = params.get(propertyPrefix + 
"autoDeleteAge");
+
+      return timeRoutedProperties;
+    }
+  }
+
+  public static class CategoryRoutedAliasProperties extends 
RoutedAliasProperties {
+    @JsonProperty("maxCardinality")
+    public Long maxCardinality;
+
+    @JsonProperty("mustMatch")
+    public String mustMatch;
+
+    @Override
+    public void validate() {
+      ensureRequiredFieldPresent(field, "'field' on category routed alias");
+    }
+
+    @Override
+    public void addRemoteMessageProperties(Map<String, Object> remoteMessage, 
String prefix) {
+      remoteMessage.put(prefix + CoreAdminParams.NAME, CATEGORY);
+      remoteMessage.put(prefix + "field", field);
+
+      if (maxCardinality != null) remoteMessage.put(prefix + "maxCardinality", 
maxCardinality);
+      if (StrUtils.isNotBlank(mustMatch)) remoteMessage.put(prefix + 
"mustMatch", mustMatch);
     }
 
-    collectionsHandler.handleRequestBody(wrapParams(obj.getRequest(), 
v1Params), obj.getResponse());
+    public static CategoryRoutedAliasProperties createFromSolrParams(
+        SolrParams params, String propertyPrefix) {
+      final CategoryRoutedAliasProperties categoryRoutedProperties =
+          new CategoryRoutedAliasProperties();
+      categoryRoutedProperties.field = params.required().get(propertyPrefix + 
"field");
+
+      categoryRoutedProperties.maxCardinality = params.getLong(propertyPrefix 
+ "maxCardinality");
+      categoryRoutedProperties.mustMatch = params.get(propertyPrefix + 
"mustMatch");
+      return categoryRoutedProperties;
+    }
+  }
+
+  /**
+   * Returns a SolrParams object containing only those values whose keys match 
a specified prefix
+   * (with that prefix removed)
+   *
+   * <p>Query-parameter based v1 APIs often mimic hierarchical parameters by 
using a prefix in the
+   * query-param key to group similar parameters together. This function can 
be used to identify all
+   * of the parameters "nested" in this way, with their prefix removed.
+   */
+  public static SolrParams getHierarchicalParametersByPrefix(
+      SolrParams paramSource, String prefix) {
+    final ModifiableSolrParams filteredParams = new ModifiableSolrParams();
+    paramSource.stream()
+        .filter(e -> e.getKey().startsWith(prefix))
+        .forEach(e -> 
filteredParams.add(e.getKey().substring(prefix.length()), e.getValue()));
+    return filteredParams;
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionAPI.java 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionAPI.java
index 1bfb176fd46..94ccfb9f183 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionAPI.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateCollectionAPI.java
@@ -49,6 +49,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -407,6 +408,14 @@ public class CreateCollectionAPI extends AdminAPIBase {
       }
     }
 
+    public void addToRemoteMessageWithPrefix(Map<String, Object> 
remoteMessage, String prefix) {
+      final Map<String, Object> v1Params = toMap(new HashMap<>());
+      convertV2CreateCollectionMapToV1ParamMap(v1Params);
+      for (Map.Entry<String, Object> v1Param : v1Params.entrySet()) {
+        remoteMessage.put(prefix + v1Param.getKey(), v1Param.getValue());
+      }
+    }
+
     /**
      * Convert a map representing the v2 request body into v1-appropriate 
query-parameters.
      *
@@ -436,7 +445,9 @@ public class CreateCollectionAPI extends AdminAPIBase {
             v2MapVals.put(CollectionAdminParams.COLL_CONF, 
v2MapVals.remove(V2ApiConstants.CONFIG));
             break;
           case SHARD_NAMES:
-            v2MapVals.put(SHARDS_PROP, v2MapVals.remove(SHARD_NAMES));
+            final String shardsValue =
+                String.join(",", (Collection<String>) 
v2MapVals.remove(SHARD_NAMES));
+            v2MapVals.put(SHARDS_PROP, shardsValue);
             break;
           case V2ApiConstants.SHUFFLE_NODES:
             v2MapVals.put(
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 3f55ee17f64..970f4d8478f 100644
--- a/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/AliasIntegrationTest.java
@@ -892,7 +892,7 @@ public class AliasIntegrationTest extends SolrCloudTestCase 
{
     new V2Request.Builder("/aliases")
         .withMethod(SolrRequest.METHOD.POST)
         .withPayload(
-            "{\"create-alias\": {\"name\": \"testalias6\", 
collections:[\"collection2\",\"collection1\"]}}")
+            "{\"name\": \"testalias6\", \"collections\": 
[\"collection2\",\"collection1\"]}")
         .build()
         .process(cluster.getSolrClient());
 
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/CreateRoutedAliasTest.java 
b/solr/core/src/test/org/apache/solr/cloud/CreateRoutedAliasTest.java
index 1254552bf9c..69f8f1adc1e 100644
--- a/solr/core/src/test/org/apache/solr/cloud/CreateRoutedAliasTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/CreateRoutedAliasTest.java
@@ -94,45 +94,41 @@ public class CreateRoutedAliasTest extends 
SolrCloudTestCase {
 
     final String baseUrl = 
cluster.getRandomJetty(random()).getBaseUrl().toString();
     // TODO fix Solr test infra so that this /____v2/ becomes /api/
+    final String aliasJson =
+        "{\n"
+            + "    \"name\": \""
+            + aliasName
+            + "\",\n"
+            + "    \"routers\" : [{\n"
+            + "      \"type\": \"time\",\n"
+            + "      \"field\": \"evt_dt\",\n"
+            + "      \"start\":\"NOW/DAY\",\n"
+            // small window for test failure once a day.
+            + "      \"interval\":\"+2HOUR\",\n"
+            + "      \"maxFutureMs\":\"14400000\"\n"
+            + "    }],\n"
+            // TODO should we use "NOW=" param?  Won't work with v2 and is 
kinda a hack any way
+            // since intended for distributed search
+            + "    \"create-collection\" : {\n"
+            + "      \"router\": {\n"
+            + "        \"name\":\"implicit\",\n"
+            + "        \"field\":\"foo_s\"\n"
+            + "      },\n"
+            + "      \"shardNames\": [\"foo\", \"bar\"],\n"
+            + "      \"config\":\"_default\",\n"
+            + "      \"tlogReplicas\":1,\n"
+            + "      \"pullReplicas\":1,\n"
+            + "      \"nodeSet\": [\""
+            + createNode
+            + "\"],\n"
+            + "      \"properties\" : {\n"
+            + "        \"foobar\":\"bazbam\",\n"
+            + "        \"foobar2\":\"bazbam2\"\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n";
     HttpPost post = new HttpPost(baseUrl + "/____v2/aliases");
-    post.setEntity(
-        new StringEntity(
-            "{\n"
-                + "  \"create-alias\" : {\n"
-                + "    \"name\": \""
-                + aliasName
-                + "\",\n"
-                + "    \"router\" : {\n"
-                + "      \"name\": \"time\",\n"
-                + "      \"field\": \"evt_dt\",\n"
-                + "      \"start\":\"NOW/DAY\",\n"
-                + // small window for test failure once a day.
-                "      \"interval\":\"+2HOUR\",\n"
-                + "      \"maxFutureMs\":\"14400000\"\n"
-                + "    },\n"
-                +
-                // TODO should we use "NOW=" param?  Won't work with v2 and is 
kinda a hack any way
-                // since intended for distributed search
-                "    \"create-collection\" : {\n"
-                + "      \"router\": {\n"
-                + "        \"name\":\"implicit\",\n"
-                + "        \"field\":\"foo_s\"\n"
-                + "      },\n"
-                + "      \"shards\":\"foo,bar\",\n"
-                + "      \"config\":\"_default\",\n"
-                + "      \"tlogReplicas\":1,\n"
-                + "      \"pullReplicas\":1,\n"
-                + "      \"nodeSet\": '"
-                + createNode
-                + "',\n"
-                + "      \"properties\" : {\n"
-                + "        \"foobar\":\"bazbam\",\n"
-                + "        \"foobar2\":\"bazbam2\"\n"
-                + "      }\n"
-                + "    }\n"
-                + "  }\n"
-                + "}",
-            ContentType.APPLICATION_JSON));
+    post.setEntity(new StringEntity(aliasJson, ContentType.APPLICATION_JSON));
     assertSuccess(post);
 
     Date startDate = DateMathParser.parseMath(new Date(), "NOW/DAY");
@@ -215,7 +211,7 @@ public class CreateRoutedAliasTest extends 
SolrCloudTestCase {
   }
 
   @Test
-  public void testUpdateRoudetedAliasDoesNotChangeCollectionList() throws 
Exception {
+  public void testUpdateRoutedAliasDoesNotChangeCollectionList() throws 
Exception {
 
     final String aliasName = getSaferTestName();
     Instant start = Instant.now().truncatedTo(ChronoUnit.HOURS); // mostly 
make sure no millis
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 3dd7182fb7a..611b7bd9d66 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
@@ -89,13 +89,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
       apiBag.registerObject(clusterAPI.commands);
     }
 
-    compareOutput(
-        apiBag,
-        "/aliases",
-        POST,
-        "{create-alias:{name: aliasName , collections:[c1,c2] }}",
-        "{operation : createalias, name: aliasName, collections:\"c1,c2\" }");
-
     compareOutput(
         apiBag, "/collections/collName", POST, "{reload:{}}", "{name:collName, 
operation :reload}");
 
@@ -264,9 +257,6 @@ public class TestCollectionAPIs extends SolrTestCaseJ4 {
       return null;
     }
 
-    @Override
-    protected void copyFromClusterProp(Map<String, Object> props, String prop) 
{}
-
     @Override
     void invokeAction(
         SolrQueryRequest req,
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 a9bb20b999f..caa312e9d0a 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
@@ -18,10 +18,6 @@ package org.apache.solr.handler.admin;
 
 import static org.apache.solr.common.params.CommonParams.ACTION;
 
-import java.util.Locale;
-import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.cloud.api.collections.CategoryRoutedAlias;
-import org.apache.solr.cloud.api.collections.RoutedAlias;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CollectionAdminParams;
 import org.apache.solr.common.params.CollectionParams;
@@ -30,7 +26,6 @@ import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.backup.BackupManager;
-import org.apache.solr.handler.admin.api.CreateAliasAPI;
 import org.apache.solr.handler.admin.api.RestoreCollectionAPI;
 import org.junit.Test;
 
@@ -52,7 +47,6 @@ public class V2CollectionsAPIMappingTest extends 
V2ApiMappingTest<CollectionsHan
 
   @Override
   public void populateApiBag() {
-    apiBag.registerObject(new CreateAliasAPI(getRequestHandler()));
     apiBag.registerObject(new RestoreCollectionAPI(getRequestHandler()));
   }
 
@@ -66,68 +60,6 @@ public class V2CollectionsAPIMappingTest extends 
V2ApiMappingTest<CollectionsHan
     return false;
   }
 
-  @Test
-  public void testCreateAliasAllProperties() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedV1Params(
-            "/aliases",
-            "POST",
-            "{'create-alias': {"
-                + "'name': 'aliasName', "
-                + "'collections': ['techproducts1', 'techproducts2'], "
-                + "'tz': 'someTimeZone', "
-                + "'async': 'requestTrackingId', "
-                + "'router': {"
-                + "    'name': 'time', "
-                + "    'field': 'date_dt', "
-                + "    'interval': '+1HOUR', "
-                + "     'maxFutureMs': 3600, "
-                + "     'preemptiveCreateMath': 
'somePreemptiveCreateMathString', "
-                + "     'autoDeleteAge': 'someAutoDeleteAgeExpression', "
-                + "     'maxCardinality': 36, "
-                + "     'mustMatch': 'someRegex', "
-                + "}, "
-                + "'create-collection': {"
-                + "     'numShards': 1, "
-                + "     'properties': {'foo': 'bar', 'foo2': 'bar2'}, "
-                + "     'replicationFactor': 3 "
-                + "}"
-                + "}}");
-
-    assertEquals(CollectionParams.CollectionAction.CREATEALIAS.lowerName, 
v1Params.get(ACTION));
-    assertEquals("aliasName", v1Params.get(CommonParams.NAME));
-    assertEquals("techproducts1,techproducts2", v1Params.get("collections"));
-    assertEquals("someTimeZone", 
v1Params.get(CommonParams.TZ.toLowerCase(Locale.ROOT)));
-    assertEquals("requestTrackingId", v1Params.get(CommonAdminParams.ASYNC));
-    assertEquals(
-        "time", 
v1Params.get(CollectionAdminRequest.CreateTimeRoutedAlias.ROUTER_TYPE_NAME));
-    assertEquals(
-        "date_dt", 
v1Params.get(CollectionAdminRequest.CreateTimeRoutedAlias.ROUTER_FIELD));
-    assertEquals(
-        "+1HOUR", 
v1Params.get(CollectionAdminRequest.CreateTimeRoutedAlias.ROUTER_INTERVAL));
-    assertEquals(
-        3600,
-        
v1Params.getPrimitiveInt(CollectionAdminRequest.CreateTimeRoutedAlias.ROUTER_MAX_FUTURE));
-    assertEquals(
-        "somePreemptiveCreateMathString",
-        
v1Params.get(CollectionAdminRequest.CreateTimeRoutedAlias.ROUTER_PREEMPTIVE_CREATE_WINDOW));
-    assertEquals(
-        "someAutoDeleteAgeExpression",
-        
v1Params.get(CollectionAdminRequest.CreateTimeRoutedAlias.ROUTER_AUTO_DELETE_AGE));
-    assertEquals(36, 
v1Params.getPrimitiveInt(CategoryRoutedAlias.ROUTER_MAX_CARDINALITY));
-    assertEquals("someRegex", 
v1Params.get(CategoryRoutedAlias.ROUTER_MUST_MATCH));
-    assertEquals(
-        1,
-        v1Params.getPrimitiveInt(
-            RoutedAlias.CREATE_COLLECTION_PREFIX + 
CollectionAdminParams.NUM_SHARDS));
-    assertEquals("bar", v1Params.get(RoutedAlias.CREATE_COLLECTION_PREFIX + 
"property.foo"));
-    assertEquals("bar2", v1Params.get(RoutedAlias.CREATE_COLLECTION_PREFIX + 
"property.foo2"));
-    assertEquals(
-        3,
-        v1Params.getPrimitiveInt(
-            RoutedAlias.CREATE_COLLECTION_PREFIX + 
ZkStateReader.REPLICATION_FACTOR));
-  }
-
   @Test
   public void testRestoreAllProperties() throws Exception {
     final SolrParams v1Params =
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java
new file mode 100644
index 00000000000..ce4b5b51544
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java
@@ -0,0 +1,348 @@
+/*
+ * 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.request.beans.V2ApiConstants.COLLECTIONS;
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.common.params.CommonAdminParams.ASYNC;
+import static org.hamcrest.Matchers.containsString;
+
+import java.util.List;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.hamcrest.MatcherAssert;
+import org.junit.Test;
+
+/** Unit tests for {@link CreateAliasAPI} */
+public class CreateAliasAPITest extends SolrTestCaseJ4 {
+
+  @Test
+  public void testReportsErrorIfRequestBodyMissing() {
+    final SolrException thrown =
+        expectThrows(
+            SolrException.class,
+            () -> {
+              final var api = new CreateAliasAPI(null, null, null);
+              api.createAlias(null);
+            });
+
+    assertEquals(400, thrown.code());
+    assertEquals("Request body is required but missing", thrown.getMessage());
+  }
+
+  @Test
+  public void testReportsErrorIfAliasNameInvalid() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "some@invalid$alias";
+    requestBody.collections = List.of("validColl1", "validColl2");
+
+    final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+    MatcherAssert.assertThat(thrown.getMessage(), containsString("Invalid 
alias"));
+    MatcherAssert.assertThat(thrown.getMessage(), 
containsString("some@invalid$alias"));
+    MatcherAssert.assertThat(
+        thrown.getMessage(),
+        containsString(
+            "alias names must consist entirely of periods, underscores, 
hyphens, and alphanumerics"));
+  }
+
+  // Aliases can be normal or "routed', but not both.
+  @Test
+  public void 
testReportsErrorIfExplicitCollectionsAndRoutingParamsBothProvided() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "validName";
+    requestBody.collections = List.of("validColl1");
+    final var categoryRouter = new 
CreateAliasAPI.CategoryRoutedAliasProperties();
+    categoryRouter.field = "someField";
+    requestBody.routers = List.of(categoryRouter);
+
+    final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+    assertEquals(
+        "Collections cannot be specified when creating a routed alias.", 
thrown.getMessage());
+  }
+
+  @Test
+  public void 
testReportsErrorIfNeitherExplicitCollectionsNorRoutingParamsProvided() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "validName";
+
+    final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+    assertEquals(400, thrown.code());
+    assertEquals(
+        "Alias creation requires either a list of either collections (for 
creating a traditional alias) or routers (for creating a routed alias)",
+        thrown.getMessage());
+  }
+
+  @Test
+  public void 
testRoutedAliasesMustProvideAConfigsetToUseOnCreatedCollections() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "validName";
+    final var categoryRouter = new 
CreateAliasAPI.CategoryRoutedAliasProperties();
+    categoryRouter.field = "someField";
+    requestBody.routers = List.of(categoryRouter);
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    requestBody.collCreationParameters = createParams;
+
+    final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+    assertEquals(400, thrown.code());
+    MatcherAssert.assertThat(
+        thrown.getMessage(), containsString("Routed alias creation requires a 
configset name"));
+  }
+
+  @Test
+  public void testRoutedAliasesMustNotSpecifyANameInCollectionCreationParams() 
{
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "validName";
+    final var categoryRouter = new 
CreateAliasAPI.CategoryRoutedAliasProperties();
+    categoryRouter.field = "someField";
+    requestBody.routers = List.of(categoryRouter);
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    createParams.config = "someConfig";
+    // Not allowed since routed-aliases-created collections have semantically 
meaningful names
+    // determined by the alias
+    createParams.name = "someCollectionName";
+    requestBody.collCreationParameters = createParams;
+
+    final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+
+    assertEquals(400, thrown.code());
+    MatcherAssert.assertThat(thrown.getMessage(), containsString("cannot 
specify the name"));
+  }
+
+  @Test
+  public void 
testReportsErrorIfCategoryRoutedAliasDoesntSpecifyAllRequiredParameters() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "validName";
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    createParams.config = "someConfig";
+    requestBody.collCreationParameters = createParams;
+    final var categoryRouter = new 
CreateAliasAPI.CategoryRoutedAliasProperties();
+    categoryRouter.maxCardinality = 123L;
+    requestBody.routers = List.of(categoryRouter);
+
+    final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+
+    assertEquals(400, thrown.code());
+    assertEquals(
+        "Missing required parameter: 'field' on category routed alias", 
thrown.getMessage());
+  }
+
+  @Test
+  public void 
testReportsErrorIfTimeRoutedAliasDoesntSpecifyAllRequiredParameters() {
+    // No 'field' defined!
+    {
+      final var timeRouter = new CreateAliasAPI.TimeRoutedAliasProperties();
+      timeRouter.start = "NOW";
+      timeRouter.interval = "+5MINUTES";
+      final var requestBody = requestBodyWithProvidedRouter(timeRouter);
+
+      final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+
+      assertEquals(400, thrown.code());
+      assertEquals("Missing required parameter: 'field' on time routed alias", 
thrown.getMessage());
+    }
+
+    // No 'start' defined!
+    {
+      final var timeRouter = new CreateAliasAPI.TimeRoutedAliasProperties();
+      timeRouter.field = "someField";
+      timeRouter.interval = "+5MINUTES";
+      final var requestBody = requestBodyWithProvidedRouter(timeRouter);
+
+      final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+
+      assertEquals(400, thrown.code());
+      assertEquals("Missing required parameter: 'start' on time routed alias", 
thrown.getMessage());
+    }
+
+    // No 'interval' defined!
+    {
+      final var timeRouter = new CreateAliasAPI.TimeRoutedAliasProperties();
+      timeRouter.field = "someField";
+      timeRouter.start = "NOW";
+      final var requestBody = requestBodyWithProvidedRouter(timeRouter);
+
+      final var thrown = expectThrows(SolrException.class, () -> 
requestBody.validate());
+
+      assertEquals(400, thrown.code());
+      assertEquals(
+          "Missing required parameter: 'interval' on time routed alias", 
thrown.getMessage());
+    }
+  }
+
+  @Test
+  public void testRemoteMessageCreationForTraditionalAlias() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "someAliasName";
+    requestBody.collections = List.of("validColl1", "validColl2");
+    requestBody.async = "someAsyncId";
+
+    final var remoteMessage =
+        
CreateAliasAPI.createRemoteMessageForTraditionalAlias(requestBody).getProperties();
+
+    assertEquals(4, remoteMessage.size());
+    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someAliasName", remoteMessage.get("name"));
+    assertEquals("validColl1,validColl2", remoteMessage.get(COLLECTIONS));
+    assertEquals("someAsyncId", remoteMessage.get(ASYNC));
+  }
+
+  @Test
+  public void testRemoteMessageCreationForCategoryRoutedAlias() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "someAliasName";
+    final var categoryRouter = new 
CreateAliasAPI.CategoryRoutedAliasProperties();
+    categoryRouter.field = "someField";
+    requestBody.routers = List.of(categoryRouter);
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    createParams.config = "someConfig";
+    requestBody.collCreationParameters = createParams;
+
+    final var remoteMessage =
+        
CreateAliasAPI.createRemoteMessageForRoutedAlias(requestBody).getProperties();
+
+    assertEquals(6, remoteMessage.size());
+    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someAliasName", remoteMessage.get("name"));
+    assertEquals("category", remoteMessage.get("router.name"));
+    assertEquals("someField", remoteMessage.get("router.field"));
+    assertEquals(3, remoteMessage.get("create-collection.numShards"));
+    assertEquals("someConfig", 
remoteMessage.get("create-collection.collection.configName"));
+  }
+
+  @Test
+  public void testRemoteMessageCreationForTimeRoutedAlias() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "someAliasName";
+    final var timeRouter = new CreateAliasAPI.TimeRoutedAliasProperties();
+    timeRouter.field = "someField";
+    timeRouter.start = "NOW";
+    timeRouter.interval = "+1MONTH";
+    timeRouter.maxFutureMs = 123456L;
+    requestBody.routers = List.of(timeRouter);
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    createParams.config = "someConfig";
+    requestBody.collCreationParameters = createParams;
+
+    final var remoteMessage =
+        
CreateAliasAPI.createRemoteMessageForRoutedAlias(requestBody).getProperties();
+
+    assertEquals(9, remoteMessage.size());
+    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someAliasName", remoteMessage.get("name"));
+    assertEquals("time", remoteMessage.get("router.name"));
+    assertEquals("someField", remoteMessage.get("router.field"));
+    assertEquals("NOW", remoteMessage.get("router.start"));
+    assertEquals("+1MONTH", remoteMessage.get("router.interval"));
+    assertEquals(Long.valueOf(123456L), 
remoteMessage.get("router.maxFutureMs"));
+    assertEquals(3, remoteMessage.get("create-collection.numShards"));
+    assertEquals("someConfig", 
remoteMessage.get("create-collection.collection.configName"));
+  }
+
+  @Test
+  public void testRemoteMessageCreationForMultiDimensionalRoutedAlias() {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "someAliasName";
+    final var timeRouter = new CreateAliasAPI.TimeRoutedAliasProperties();
+    timeRouter.field = "someField";
+    timeRouter.start = "NOW";
+    timeRouter.interval = "+1MONTH";
+    timeRouter.maxFutureMs = 123456L;
+    final var categoryRouter = new 
CreateAliasAPI.CategoryRoutedAliasProperties();
+    categoryRouter.field = "someField";
+    requestBody.routers = List.of(timeRouter, categoryRouter);
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    createParams.config = "someConfig";
+    requestBody.collCreationParameters = createParams;
+
+    final var remoteMessage =
+        
CreateAliasAPI.createRemoteMessageForRoutedAlias(requestBody).getProperties();
+
+    assertEquals(11, remoteMessage.size());
+    assertEquals("createalias", remoteMessage.get(QUEUE_OPERATION));
+    assertEquals("someAliasName", remoteMessage.get("name"));
+    assertEquals("time", remoteMessage.get("router.0.name"));
+    assertEquals("someField", remoteMessage.get("router.0.field"));
+    assertEquals("NOW", remoteMessage.get("router.0.start"));
+    assertEquals("+1MONTH", remoteMessage.get("router.0.interval"));
+    assertEquals(Long.valueOf(123456L), 
remoteMessage.get("router.0.maxFutureMs"));
+    assertEquals("category", remoteMessage.get("router.1.name"));
+    assertEquals("someField", remoteMessage.get("router.1.field"));
+    assertEquals(3, remoteMessage.get("create-collection.numShards"));
+    assertEquals("someConfig", 
remoteMessage.get("create-collection.collection.configName"));
+  }
+
+  private CreateAliasAPI.CreateAliasRequestBody requestBodyWithProvidedRouter(
+      CreateAliasAPI.RoutedAliasProperties router) {
+    final var requestBody = new CreateAliasAPI.CreateAliasRequestBody();
+    requestBody.name = "validName";
+    final var createParams = new 
CreateCollectionAPI.CreateCollectionRequestBody();
+    createParams.numShards = 3;
+    createParams.config = "someConfig";
+    requestBody.collCreationParameters = createParams;
+
+    requestBody.routers = List.of(router);
+
+    return requestBody;
+  }
+
+  @Test
+  public void testConvertsV1ParamsForMultiDimensionalAliasToV2RequestBody() {
+    final var v1Params = new ModifiableSolrParams();
+    v1Params.add("name", "someAliasName");
+    v1Params.add("router.name", "Dimensional[time,category]");
+    v1Params.add("router.0.field", "someField");
+    v1Params.add("router.0.start", "NOW");
+    v1Params.add("router.0.interval", "+1MONTH");
+    v1Params.add("router.0.maxFutureMs", "123456");
+    v1Params.add("router.1.field", "someOtherField");
+    v1Params.add("router.1.maxCardinality", "20");
+    v1Params.add("create-collection.numShards", "3");
+    v1Params.add("create-collection.collection.configName", "someConfig");
+
+    final var requestBody = CreateAliasAPI.createFromSolrParams(v1Params);
+
+    assertEquals("someAliasName", requestBody.name);
+    assertEquals(2, requestBody.routers.size());
+    assertTrue(
+        "Incorrect router type " + requestBody.routers.get(0) + " at index 0",
+        requestBody.routers.get(0) instanceof 
CreateAliasAPI.TimeRoutedAliasProperties);
+    final var timeRouter = (CreateAliasAPI.TimeRoutedAliasProperties) 
requestBody.routers.get(0);
+    assertEquals("someField", timeRouter.field);
+    assertEquals("NOW", timeRouter.start);
+    assertEquals("+1MONTH", timeRouter.interval);
+    assertEquals(Long.valueOf(123456L), timeRouter.maxFutureMs);
+    assertTrue(
+        "Incorrect router type " + requestBody.routers.get(1) + " at index 1",
+        requestBody.routers.get(1) instanceof 
CreateAliasAPI.CategoryRoutedAliasProperties);
+    final var categoryRouter =
+        (CreateAliasAPI.CategoryRoutedAliasProperties) 
requestBody.routers.get(1);
+    assertEquals("someOtherField", categoryRouter.field);
+    assertEquals(Long.valueOf(20), categoryRouter.maxCardinality);
+    final var createCollParams = requestBody.collCreationParameters;
+    assertEquals(Integer.valueOf(3), createCollParams.numShards);
+    assertEquals("someConfig", createCollParams.config);
+  }
+  // v1 -> v2 param conversion test
+}
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 f8093939515..2853a039e5a 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
@@ -45,7 +45,7 @@ While it is possible to send updates to an alias spanning 
multiple collections,
 
 *Routed aliases* are aliases with additional capabilities to act as a kind of 
super-collection that route updates to the correct collection.
 
-Routing is data driven and may be based on a temporal field or on categories   
specified in a field (normally string based).
+Routing is data driven and may be based on a temporal field or on categories 
specified in a field (normally string based).
 See xref:aliases.adoc#routed-aliases[Routed Aliases] for some important 
high-level information before getting started.
 
 [source,text]
@@ -115,7 +115,16 @@ Most routed alias parameters become _alias properties_ 
that can subsequently be
 CREATEALIAS will validate against many (but not all) bad values, whereas 
ALIASPROP blindly accepts any key or value you give it.
 Some "valid" modifications allowed by CREATEALIAS may still be unwise, see 
notes below. "Expert only" modifications are technically possible, but require 
good understanding of how the code works and may require several precursor 
operations.
 
-`router.name`::
+Routed aliases currently support up to two "dimensions" of routing, with each 
dimension being either a "time" or "category"-based.
+Each dimension takes a number of parameters, which vary based on its type.
+
+On v1 requests, routing-dimension parameters are grouped together by 
query-parameter prefix.
+A routed alias with only one dimension uses the `router.` prefix for its 
parameters (e.g. `router.field`).
+Two-dimensional routed aliases add a number to this query-parameter prefix to 
distinguish which routing-dimension the parameter belongs to (e.g. 
`router.0.name`, `router.1.field`).
+
+On v2 requests, routing-dimensions are specified as individual objects within 
a list (e.g. `[{"type": "category", "field": "manu_id_s"}]`).
+
+`router.name` (v1), `type` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -124,6 +133,7 @@ s|Required |Default: none |Modify: Do not change after 
creation
 +
 The type of routing to use.
 Presently only `time` and `category` and `Dimensional[]` are valid.
+v2 requests only allow `time` or `category` since dimensionality information 
lives in the `routers` list unique to v2 requests (though the caveats below 
about dimension ordering still apply).
 +
 In the case of a 
xref:aliases.adoc#dimensional-routed-aliases[multi-dimensional routed alias] 
(aka "DRA"), it is required to express all the dimensions in the same order 
that they will appear in the dimension
 array.
@@ -134,7 +144,7 @@ Careful design of dimensional routing is required to avoid 
an explosion in the n
 Solr Cloud may have difficulty managing more than a thousand collections.
 See examples below for further clarification on how to configure individual 
dimensions.
 
-`router.field`::
+`router.field` (v1), `field` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -156,9 +166,11 @@ All other fields are identical in requirements and naming 
except that we insist
 The configset must be created beforehand, either uploaded or copied and 
modified.
 It's probably a bad idea to use "data driven" mode as schema mutations might 
happen concurrently leading to errors.
 
+On v2 requests, `create-collection` takes a JSON object containing all 
provided collection-creation parameters (e.g. `"create-collection": { 
"numShards": 3, "config": "_default"}`).
+
 ==== Time Routed Alias Parameters
 
-`router.start`::
+`router.start` (v2), `start` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -172,7 +184,7 @@ If a document is submitted with an earlier value for 
`router.field` then the ear
 This date/time MUST NOT have a milliseconds component other than 0.
 Particularly, this means `NOW` will fail 999 times out of 1000, though 
`NOW/SECOND`, `NOW/MINUTE`, etc., will work just fine.
 
-`TZ`::
+`TZ` (v1), `tz` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -186,7 +198,7 @@ as an alias property.
 If GMT-4 is supplied for this value then a document dated 
2018-01-14T21:00:00:01.2345Z would be stored in the myAlias_2018-01-15_01 
collection (assuming an interval of +1HOUR).
 
 
-`router.interval`::
+`router.interval` (v1), `interval` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -196,7 +208,7 @@ s|Required |Default: none | Modify: Yes
 A date math expression that will be appended to a timestamp to determine the 
next collection in the series.
 Any date math expression that can be evaluated if appended to a timestamp of 
the form 2018-01-15T16:17:18 will work here.
 
-`router.maxFutureMs`::
+`router.maxFutureMs` (v1), `maxFutureMs` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -206,7 +218,7 @@ Any date math expression that can be evaluated if appended 
to a timestamp of the
 The maximum milliseconds into the future that a document is allowed to have in 
`router.field` for it to be accepted without error.
 If there was no limit, then an erroneous value could trigger many collections 
to be created.
 
-`router.preemptiveCreateMath`::
+`router.preemptiveCreateMath` (v1), `preemptiveCreateMath` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -233,7 +245,7 @@ Example: `90MINUTES`.
 +
 This property is empty by default indicating just-in-time, synchronous 
creation of new collections.
 
-`router.autoDeleteAge`::
+`router.autoDeleteAge` (v1), `autoDeleteAge` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -251,7 +263,7 @@ The default is not to delete.
 
 ==== Category Routed Alias Parameters
 
-`router.maxCardinality`::
+`router.maxCardinality` (v1), `maxCardinality` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -261,7 +273,7 @@ The default is not to delete.
 The maximum number of categories allowed for this alias.
 This setting safeguards against the inadvertent creation of an infinite number 
of collections in the event of bad data.
 
-`router.mustMatch`::
+`router.mustMatch` (v1), `mustMatch` (v2)::
 +
 [%autowidth,frame=none]
 |===
@@ -277,14 +289,14 @@ Overly complex patterns will produce CPU or garbage 
collection overhead during i
 
 ==== Dimensional Routed Alias Parameters
 
-`router.#.`::
+`router.#.` (v1)::
 +
 [%autowidth,frame=none]
 |===
 |Optional |Default: none | Modify: As per above
 |===
 +
-This prefix denotes which position in the dimension array is being referred to 
for purposes of dimension configuration.
+A prefix used on v1 request parameters to associate the parameter with a 
particular dimensional, in multi-dimensional aliases.
 +
 For example in a `Dimensional[time,category]` alias, `router.0.start` would be 
used to set the start time for the time dimension.
 
@@ -332,12 +344,10 @@ 
http://localhost:8983/solr/admin/collections?action=CREATEALIAS&name=testalias&c
 
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type: 
application/json' -d '
+curl -X POST http://localhost:8983/api/aliases -H 'Content-Type: 
application/json' -d '
   {
-    "create-alias":{
-      "name":"testalias",
-      "collections":["foo","bar"]
-    }
+    "name":"testalias",
+    "collections":["foo","bar"]
   }
 '
 ----
@@ -408,32 +418,32 @@ 
http://localhost:8983/solr/admin/collections?action=CREATEALIAS
 
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type: 
application/json' -d '
+curl -X POST http://localhost:8983/api/aliases -H 'Content-Type: 
application/json' -d '
   {
-    "create-alias" : {
       "name": "somethingTemporalThisWayComes",
-      "router" : {
-        "name": "time",
-        "field": "evt_dt",
-        "start":"NOW/MINUTE",
-        "interval":"+2HOUR",
-        "maxFutureMs":"14400000"
-      },
+      "routers" : [
+        {
+          "type": "time",
+          "field": "evt_dt",
+          "start":"NOW/MINUTE",
+          "interval":"+2HOUR",
+          "maxFutureMs":"14400000"
+        }
+      ]
       "create-collection" : {
         "config":"_default",
         "router": {
           "name":"implicit",
           "field":"foo_s"
         },
-        "shards":"foo,bar,baz",
+        "shardNames": ["foo", "bar", "baz"],
         "numShards": 3,
         "tlogReplicas":1,
         "pullReplicas":1,
         "properties" : {
           "foobar":"bazbam"
         }
-      }
-    }
+     }  
   }
 '
 ----
@@ -502,26 +512,26 @@ 
http://localhost:8983/solr/admin/collections?action=CREATEALIAS
 
 [source,bash]
 ----
-curl -X POST http://localhost:8983/api/collections -H 'Content-Type: 
application/json' -d '
+curl -X POST http://localhost:8983/api/aliases -H 'Content-Type: 
application/json' -d '
   {
-    "create-alias":{
-      "name":"dra_test1",
-      "router": {
-        "name": "Dimensional[time,category]",
-        "routerList" : [ {
-              "field":"myDate_tdt",
-              "start":"2019-01-01T00:00:00Z",
-              "interval":"+1MONTH",
-              "maxFutureMs":600000
-          },{
-               "field":"myCategory_s",
-               "maxCardinality":20
-          }]
+    "name":"dra_test1",
+    "routers": [
+      {
+        "type": "time",
+        "field":"myDate_tdt",
+        "start":"2019-01-01T00:00:00Z",
+        "interval":"+1MONTH",
+        "maxFutureMs":600000
       },
-      "create-collection": {
-        "config":"_default",
-        "numShards":2
+      {
+        "type": "category",
+        "field":"myCategory_s",
+        "maxCardinality":20
       }
+    ]
+    "create-collection": {
+      "config":"_default",
+      "numShards":2
     }
   }
 '
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateAliasPayload.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateAliasPayload.java
deleted file mode 100644
index b34e6e56dc5..00000000000
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateAliasPayload.java
+++ /dev/null
@@ -1,63 +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 static 
org.apache.solr.client.solrj.request.beans.V2ApiConstants.CREATE_COLLECTION_KEY;
-
-import java.util.List;
-import java.util.Map;
-import org.apache.solr.common.annotation.JsonProperty;
-import org.apache.solr.common.util.ReflectMapWriter;
-
-public class CreateAliasPayload implements ReflectMapWriter {
-  @JsonProperty(required = true)
-  public String name;
-
-  @JsonProperty public List<String> collections;
-
-  @JsonProperty public AliasRouter router;
-
-  @JsonProperty public String tz;
-
-  @JsonProperty(CREATE_COLLECTION_KEY)
-  public Map<String, Object> createCollectionParams;
-
-  @JsonProperty public String async;
-
-  public static class AliasRouter implements ReflectMapWriter {
-    @JsonProperty(required = true)
-    public String name;
-
-    @JsonProperty public String field;
-
-    @JsonProperty public String start;
-
-    @JsonProperty public String interval;
-
-    @JsonProperty public Long maxFutureMs;
-
-    @JsonProperty public String preemptiveCreateMath;
-
-    @JsonProperty public String autoDeleteAge;
-
-    @JsonProperty public Integer maxCardinality;
-
-    @JsonProperty public String mustMatch;
-
-    @JsonProperty public List<Map<String, Object>> routerList;
-  }
-}

Reply via email to