You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by no...@apache.org on 2017/01/31 05:57:55 UTC

[5/5] lucene-solr:master: SOLR-8029: Added new style APIs and a framework for creating new APIs and mapping old APIs to new

SOLR-8029: Added new style APIs and a framework for creating new APIs and mapping old APIs to new


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/71abe130
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/71abe130
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/71abe130

Branch: refs/heads/master
Commit: 71abe130697b0f279d6e3613145f1f8f052c7848
Parents: c8edbe8
Author: Noble Paul <no...@apache.org>
Authored: Tue Jan 31 16:27:33 2017 +1030
Committer: Noble Paul <no...@apache.org>
Committed: Tue Jan 31 16:27:33 2017 +1030

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   3 +
 solr/core/src/java/org/apache/solr/api/Api.java |  67 ++++
 .../src/java/org/apache/solr/api/ApiBag.java    | 354 ++++++++++++++++++
 .../java/org/apache/solr/api/ApiSupport.java    |  46 +++
 .../java/org/apache/solr/api/SpecProvider.java  |  25 ++
 .../java/org/apache/solr/api/V2HttpCall.java    | 340 +++++++++++++++++
 .../java/org/apache/solr/api/package-info.java  |  21 ++
 .../src/java/org/apache/solr/cloud/Assign.java  |  10 +-
 .../org/apache/solr/cloud/CreateShardCmd.java   |   2 +-
 .../java/org/apache/solr/core/PluginBag.java    |  71 +++-
 .../org/apache/solr/handler/BlobHandler.java    |  14 +-
 .../apache/solr/handler/DumpRequestHandler.java |  11 +
 .../apache/solr/handler/PingRequestHandler.java |   5 +
 .../apache/solr/handler/RealTimeGetHandler.java |  14 +
 .../apache/solr/handler/RequestHandlerBase.java |  12 +-
 .../org/apache/solr/handler/SchemaHandler.java  |  35 +-
 .../apache/solr/handler/SolrConfigHandler.java  |  16 +
 .../solr/handler/UpdateRequestHandler.java      |   2 +
 .../solr/handler/UpdateRequestHandlerApi.java   |  73 ++++
 .../handler/admin/BaseHandlerApiSupport.java    | 236 ++++++++++++
 .../handler/admin/CollectionHandlerApi.java     | 319 ++++++++++++++++
 .../solr/handler/admin/CollectionsHandler.java  |  69 ++--
 .../solr/handler/admin/ConfigSetsHandler.java   |  47 ++-
 .../handler/admin/ConfigSetsHandlerApi.java     | 112 ++++++
 .../solr/handler/admin/CoreAdminHandler.java    |  14 +
 .../solr/handler/admin/CoreAdminHandlerApi.java | 175 +++++++++
 .../apache/solr/handler/admin/InfoHandler.java  |  78 ++--
 .../solr/handler/admin/SecurityConfHandler.java |  70 ++++
 .../apache/solr/request/SolrQueryRequest.java   |  24 ++
 .../solr/request/SolrQueryRequestBase.java      |  35 ++
 .../org/apache/solr/schema/SchemaManager.java   |  13 +-
 .../apache/solr/security/BasicAuthPlugin.java   |  10 +-
 .../security/RuleBasedAuthorizationPlugin.java  |  11 +-
 .../security/Sha256AuthenticationProvider.java  |   8 +
 .../org/apache/solr/servlet/HttpSolrCall.java   | 115 ++++--
 .../org/apache/solr/servlet/ResponseUtils.java  |   6 +
 .../apache/solr/servlet/SolrDispatchFilter.java |  18 +-
 .../apache/solr/servlet/SolrRequestParsers.java |  27 +-
 .../org/apache/solr/util/CommandOperation.java  |   4 +
 .../apache/solr/util/JsonSchemaValidator.java   | 370 +++++++++++++++++++
 .../src/java/org/apache/solr/util/PathTrie.java | 195 ++++++++++
 solr/core/src/resources/ImplicitPlugins.json    |   6 +-
 .../src/resources/apispec/cluster.Commands.json |  74 ++++
 .../apispec/cluster.commandstatus.delete.json   |  10 +
 .../apispec/cluster.commandstatus.json          |  20 +
 .../apispec/cluster.configs.Commands.json       |  34 ++
 .../apispec/cluster.configs.delete.json         |  12 +
 .../src/resources/apispec/cluster.configs.json  |  12 +
 solr/core/src/resources/apispec/cluster.json    |  14 +
 .../src/resources/apispec/cluster.nodes.json    |  12 +
 .../cluster.security.BasicAuth.Commands.json    |  23 ++
 ...cluster.security.RuleBasedAuthorization.json | 129 +++++++
 ...luster.security.authentication.Commands.json |  12 +
 .../cluster.security.authentication.json        |  12 +
 ...cluster.security.authorization.Commands.json |  13 +
 .../apispec/cluster.security.authorization.json |  13 +
 .../resources/apispec/collections.Commands.json | 206 +++++++++++
 .../collections.collection.Commands.json        | 137 +++++++
 .../collections.collection.Commands.modify.json |  36 ++
 .../collections.collection.Commands.reload.json |  11 +
 .../apispec/collections.collection.delete.json  |  13 +
 .../apispec/collections.collection.json         |  19 +
 .../collections.collection.shards.Commands.json | 109 ++++++
 ...ctions.collection.shards.shard.Commands.json |  24 ++
 ...lections.collection.shards.shard.delete.json |  27 ++
 ....collection.shards.shard.replica.delete.json |  39 ++
 .../core/src/resources/apispec/collections.json |  13 +
 .../src/resources/apispec/core.RealtimeGet.json |  26 ++
 .../apispec/core.SchemaEdit.addCopyField.json   |  27 ++
 .../apispec/core.SchemaEdit.addField.json       |  98 +++++
 .../core.SchemaEdit.addFieldType.analyzers.json |  51 +++
 .../apispec/core.SchemaEdit.addFieldType.json   |  53 +++
 .../core.SchemaEdit.deleteCopyField.json        |  19 +
 .../core.SchemaEdit.deleteDynamicField.json     |  12 +
 .../apispec/core.SchemaEdit.deleteField.json    |  12 +
 .../core.SchemaEdit.deleteFieldType.json        |  14 +
 .../src/resources/apispec/core.SchemaEdit.json  |  47 +++
 .../apispec/core.SchemaRead.copyFields.json     |  26 ++
 ...ore.SchemaRead.dynamicFields_fieldTypes.json |  20 +
 .../apispec/core.SchemaRead.fields.json         |  34 ++
 .../src/resources/apispec/core.SchemaRead.json  |  18 +
 .../core/src/resources/apispec/core.Update.json |  17 +
 ...g.Commands.addRequestHandler.properties.json |  25 ++
 .../apispec/core.config.Commands.generic.json   |  19 +
 .../resources/apispec/core.config.Commands.json | 215 +++++++++++
 .../core.config.Commands.runtimeLib.json        |  23 ++
 .../apispec/core.config.Params.Commands.json    |  31 ++
 .../resources/apispec/core.config.Params.json   |  13 +
 .../core/src/resources/apispec/core.config.json |  18 +
 .../src/resources/apispec/core.system.blob.json |  20 +
 .../apispec/core.system.blob.upload.json        |  12 +
 .../src/resources/apispec/cores.Commands.json   |  85 +++++
 .../src/resources/apispec/cores.Status.json     |  20 +
 .../resources/apispec/cores.core.Commands.json  | 136 +++++++
 .../apispec/cores.core.Commands.split.json      |  34 ++
 solr/core/src/resources/apispec/emptySpec.json  |  11 +
 .../src/resources/apispec/node.Commands.json    |  24 ++
 solr/core/src/resources/apispec/node.Info.json  |  11 +
 .../core/src/resources/apispec/node.invoke.json |  16 +
 .../conf/solrconfig-managed-schema.xml          |   2 +-
 .../test/org/apache/solr/api/TestPathTrie.java  |  61 +++
 .../org/apache/solr/cloud/rule/RulesTest.java   |  18 +
 .../solr/core/BlobStoreTestRequestHandler.java  |   1 +
 .../test/org/apache/solr/core/SolrCoreTest.java |   1 +
 .../apache/solr/core/TestDynamicLoading.java    |   2 +-
 .../apache/solr/core/TestSolrConfigHandler.java |  85 ++++-
 .../solr/handler/V2ApiIntegrationTest.java      |  98 +++++
 .../solr/handler/admin/TestApiFramework.java    | 219 +++++++++++
 .../solr/handler/admin/TestCollectionAPIs.java  | 231 ++++++++++++
 .../solr/handler/admin/TestConfigsApi.java      |  59 +++
 .../solr/handler/admin/TestCoreAdminApis.java   | 115 ++++++
 .../solr/rest/schema/TestBulkSchemaAPI.java     |  15 +
 .../solr/security/BasicAuthIntegrationTest.java |   4 +
 .../TestRuleBasedAuthorizationPlugin.java       |   8 +-
 .../solr/servlet/SolrRequestParserTest.java     |   4 +-
 .../org/apache/solr/util/JsonValidatorTest.java | 189 ++++++++++
 .../conf/solrconfig.xml                         |   8 +-
 .../apache/solr/client/solrj/SolrRequest.java   |  13 +-
 .../solr/client/solrj/impl/CloudSolrClient.java |  16 +-
 .../org/apache/solr/common/cloud/Replica.java   |   3 +
 .../org/apache/solr/common/util/StrUtils.java   |  10 +-
 .../java/org/apache/solr/common/util/Utils.java |   5 +-
 .../solr/common/util/ValidatingJsonMap.java     | 349 +++++++++++++++++
 .../solrj/embedded/SolrExampleJettyTest.java    |   9 +-
 .../solr/common/util/TestValidatingJsonMap.java |  52 +++
 .../org/apache/solr/util/RestTestHarness.java   |   8 +
 126 files changed, 6715 insertions(+), 164 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 27a9c7f..754c971 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -118,6 +118,9 @@ New Features
 * SOLR-9481: Authentication and Authorization plugins now work in standalone mode if security.json is placed in
   SOLR_HOME on every node. Editing config through API is supported but affects only that one node. (janhoy)
 
+* SOLR-8029: Added new style APIs and a framework for creating new APIs and mapping old APIs to new
+  (noble, Steve Rowe, Cassandra Targett, Timothy Potter)
+
 Bug Fixes
 ----------------------
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/api/Api.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/api/Api.java b/solr/core/src/java/org/apache/solr/api/Api.java
new file mode 100644
index 0000000..8512c89
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/Api.java
@@ -0,0 +1,67 @@
+/*
+ * 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.api;
+
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.JsonSchemaValidator;
+
+/** Every version 2 API must extend the this class. It's mostly like a request handler
+ * but it has extra methods to provide the json schema of the end point
+ *
+ */
+public abstract class Api implements SpecProvider {
+  protected SpecProvider spec;
+  protected volatile Map<String, JsonSchemaValidator> commandSchema;
+
+  protected Api(SpecProvider spec) {
+    this.spec = spec;
+  }
+
+  /**This method helps to cache the schema validator object
+   */
+  public Map<String, JsonSchemaValidator> getCommandSchema() {
+    if (commandSchema == null) {
+      synchronized (this) {
+        if(commandSchema == null) {
+          ValidatingJsonMap commands = getSpec().getMap("commands", null);
+          commandSchema = commands != null ?
+              ImmutableMap.copyOf(ApiBag.getParsedSchema(commands)) :
+              ImmutableMap.of();
+        }
+      }
+    }
+    return commandSchema;
+  }
+
+  /** The method that gets called for each request
+   */
+  public abstract void call(SolrQueryRequest req , SolrQueryResponse rsp);
+
+  /**Get the specification of the API as a Map
+   */
+  @Override
+  public ValidatingJsonMap getSpec() {
+    return spec.getSpec();
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/api/ApiBag.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/api/ApiBag.java b/solr/core/src/java/org/apache/solr/api/ApiBag.java
new file mode 100644
index 0000000..82d6a39
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/ApiBag.java
@@ -0,0 +1,354 @@
+/*
+ * 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.api;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.core.PluginBag;
+import org.apache.solr.core.PluginInfo;
+import org.apache.solr.handler.RequestHandlerUtils;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestHandler;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.AuthorizationContext;
+import org.apache.solr.security.PermissionNameProvider;
+import org.apache.solr.util.CommandOperation;
+import org.apache.solr.util.JsonSchemaValidator;
+import org.apache.solr.util.PathTrie;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.solr.client.solrj.SolrRequest.SUPPORTED_METHODS;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.common.util.StrUtils.formatString;
+import static org.apache.solr.common.util.ValidatingJsonMap.ENUM_OF;
+import static org.apache.solr.common.util.ValidatingJsonMap.NOT_NULL;
+
+public class ApiBag {
+  private final boolean isCoreSpecific;
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final Map<String, PathTrie<Api>> apis = new ConcurrentHashMap<>();
+
+  public ApiBag(boolean isCoreSpecific) {
+    this.isCoreSpecific = isCoreSpecific;
+  }
+
+  public synchronized void register(Api api, Map<String, String> nameSubstitutes) {
+    try {
+      validateAndRegister(api, nameSubstitutes);
+    } catch (Exception e) {
+      log.error("Unable to register plugin:" + api.getClass().getName() + "with spec :" + Utils.toJSONString(api.getSpec()), e);
+      if (e instanceof RuntimeException) {
+        throw (RuntimeException) e;
+      } else {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
+      }
+
+    }
+  }
+
+  private void validateAndRegister(Api api, Map<String, String> nameSubstitutes) {
+    ValidatingJsonMap spec = api.getSpec();
+    Api introspect = new IntrospectApi(api, isCoreSpecific);
+    List<String> methods = spec.getList("methods", ENUM_OF, SUPPORTED_METHODS);
+    for (String method : methods) {
+      PathTrie<Api> registry = apis.get(method);
+
+      if (registry == null) apis.put(method, registry = new PathTrie<>(ImmutableSet.of("_introspect")));
+      ValidatingJsonMap url = spec.getMap("url", NOT_NULL);
+      ValidatingJsonMap params = url.getMap("params", null);
+      if (params != null) {
+        for (Object o : params.keySet()) {
+          ValidatingJsonMap param = params.getMap(o.toString(), NOT_NULL);
+          param.get("type", ENUM_OF, KNOWN_TYPES);
+        }
+      }
+      List<String> paths = url.getList("paths", NOT_NULL);
+      ValidatingJsonMap parts = url.getMap("parts", null);
+      if (parts != null) {
+        Set<String> wildCardNames = getWildCardNames(paths);
+        for (Object o : parts.keySet()) {
+          if (!wildCardNames.contains(o.toString()))
+            throw new RuntimeException("" + o + " is not a valid part name");
+          ValidatingJsonMap pathMeta = parts.getMap(o.toString(), NOT_NULL);
+          pathMeta.get("type", ENUM_OF, ImmutableSet.of("enum", "string", "int", "number", "boolean"));
+        }
+      }
+      verifyCommands(api.getSpec());
+      for (String path : paths) {
+        registry.insert(path, nameSubstitutes, api);
+        registerIntrospect(nameSubstitutes, registry, path, introspect);
+      }
+    }
+  }
+
+  public static void registerIntrospect(Map<String, String> nameSubstitutes, PathTrie<Api> registry, String path, Api introspect) {
+    List<String> l = PathTrie.getPathSegments(path);
+    registerIntrospect(l, registry, nameSubstitutes, introspect);
+    int lastIdx = l.size() - 1;
+    for (int i = lastIdx; i >= 0; i--) {
+      String itemAt = l.get(i);
+      if (PathTrie.templateName(itemAt) == null) break;
+      l.remove(i);
+      if (registry.lookup(l, new HashMap<>()) != null) break;
+      registerIntrospect(l, registry, nameSubstitutes, introspect);
+    }
+  }
+
+  static void registerIntrospect(List<String> l, PathTrie<Api> registry, Map<String, String> substitutes, Api introspect) {
+    ArrayList<String> copy = new ArrayList<>(l);
+    copy.add("_introspect");
+    registry.insert(copy, substitutes, introspect);
+  }
+
+  public static class IntrospectApi extends Api {
+    Api baseApi;
+    final boolean isCoreSpecific;
+
+    public IntrospectApi(Api base, boolean isCoreSpecific) {
+      super(EMPTY_SPEC);
+      this.baseApi = base;
+      this.isCoreSpecific = isCoreSpecific;
+    }
+
+    public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+
+      String cmd = req.getParams().get("command");
+      ValidatingJsonMap result = null;
+      if (cmd == null) {
+        result = isCoreSpecific ? ValidatingJsonMap.getDeepCopy(baseApi.getSpec(), 5, true) : baseApi.getSpec();
+      } else {
+        ValidatingJsonMap specCopy = ValidatingJsonMap.getDeepCopy(baseApi.getSpec(), 5, true);
+        ValidatingJsonMap commands = specCopy.getMap("commands", null);
+        if (commands != null) {
+          ValidatingJsonMap m = commands.getMap(cmd, null);
+          specCopy.put("commands", Collections.singletonMap(cmd, m));
+        }
+        result = specCopy;
+      }
+      if (isCoreSpecific) {
+        List<String> pieces = req.getHttpSolrCall() == null ? null : ((V2HttpCall) req.getHttpSolrCall()).pieces;
+        if (pieces != null) {
+          String prefix = "/" + pieces.get(0) + "/" + pieces.get(1);
+          List<String> paths = result.getMap("url", NOT_NULL).getList("paths", NOT_NULL);
+          result.getMap("url", NOT_NULL).put("paths",
+              paths.stream()
+                  .map(s -> prefix + s)
+                  .collect(Collectors.toList()));
+        }
+      }
+      List l = (List) rsp.getValues().get("spec");
+      if (l == null) rsp.getValues().add("spec", l = new ArrayList());
+      l.add(result);
+      RequestHandlerUtils.addExperimentalFormatWarning(rsp);
+    }
+  }
+
+  public static Map<String, JsonSchemaValidator> getParsedSchema(ValidatingJsonMap commands) {
+    Map<String, JsonSchemaValidator> validators = new HashMap<>();
+    for (Object o : commands.entrySet()) {
+      Map.Entry cmd = (Map.Entry) o;
+      try {
+        validators.put((String) cmd.getKey(), new JsonSchemaValidator((Map) cmd.getValue()));
+      } catch (Exception e) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in api spec", e);
+      }
+    }
+    return validators;
+  }
+
+
+  private void verifyCommands(ValidatingJsonMap spec) {
+    ValidatingJsonMap commands = spec.getMap("commands", null);
+    if (commands == null) return;
+    getParsedSchema(commands);
+
+  }
+
+  private Set<String> getWildCardNames(List<String> paths) {
+    Set<String> wildCardNames = new HashSet<>();
+    for (String path : paths) {
+      List<String> p = PathTrie.getPathSegments(path);
+      for (String s : p) {
+        String wildCard = PathTrie.templateName(s);
+        if (wildCard != null) wildCardNames.add(wildCard);
+      }
+    }
+    return wildCardNames;
+  }
+
+
+  public Api lookup(String path, String httpMethod, Map<String, String> parts) {
+    if (httpMethod == null) {
+      for (PathTrie<Api> trie : apis.values()) {
+        Api api = trie.lookup(path, parts);
+        if (api != null) return api;
+      }
+      return null;
+    } else {
+      PathTrie<Api> registry = apis.get(httpMethod);
+      if (registry == null) return null;
+      return registry.lookup(path, parts);
+    }
+  }
+
+  public static SpecProvider getSpec(final String name) {
+    return () -> {
+      return ValidatingJsonMap.parse(APISPEC_LOCATION + name + ".json", APISPEC_LOCATION);
+    };
+  }
+
+  public static class ReqHandlerToApi extends Api implements PermissionNameProvider {
+    SolrRequestHandler rh;
+
+    public ReqHandlerToApi(SolrRequestHandler rh, SpecProvider spec) {
+      super(spec);
+      this.rh = rh;
+    }
+
+    @Override
+    public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+      rh.handleRequest(req, rsp);
+    }
+
+    @Override
+    public Name getPermissionName(AuthorizationContext ctx) {
+      if (rh instanceof PermissionNameProvider) {
+        return ((PermissionNameProvider) rh).getPermissionName(ctx);
+      }
+      return null;
+    }
+  }
+
+  public static List<Api> wrapRequestHandlers(final SolrRequestHandler rh, String... specs) {
+    ImmutableList.Builder<Api> b = ImmutableList.builder();
+    for (String spec : specs) b.add(new ReqHandlerToApi(rh, ApiBag.getSpec(spec)));
+    return b.build();
+  }
+
+  public static final String APISPEC_LOCATION = "apispec/";
+  public static final String INTROSPECT = "/_introspect";
+
+
+  public static final SpecProvider EMPTY_SPEC = () -> ValidatingJsonMap.EMPTY;
+  public static final String HANDLER_NAME = "handlerName";
+  public static final Set<String> KNOWN_TYPES = ImmutableSet.of("string", "boolean", "list", "int", "double", "object");
+
+  public PathTrie<Api> getRegistry(String method) {
+    return apis.get(method);
+  }
+
+  public void registerLazy(PluginBag.PluginHolder<SolrRequestHandler> holder, PluginInfo info) {
+    String specName = info.attributes.get("spec");
+    if (specName == null) specName = "emptySpec";
+    register(new LazyLoadedApi(ApiBag.getSpec(specName), holder), Collections.singletonMap(HANDLER_NAME, info.attributes.get(NAME)));
+  }
+
+  public static SpecProvider constructSpec(PluginInfo info) {
+    Object specObj = info == null ? null : info.attributes.get("spec");
+    if (specObj == null) specObj = "emptySpec";
+    if (specObj instanceof Map) {
+      Map map = (Map) specObj;
+      return () -> ValidatingJsonMap.getDeepCopy(map, 4, false);
+    } else {
+      return ApiBag.getSpec((String) specObj);
+    }
+  }
+
+  public static List<CommandOperation> getCommandOperations(Reader reader, Map<String, JsonSchemaValidator> validators, boolean validate) {
+    List<CommandOperation> parsedCommands = null;
+    try {
+      parsedCommands = CommandOperation.parse(reader);
+    } catch (IOException e) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
+    }
+    if (validators == null || !validate) {    // no validation possible because we do not have a spec
+      return parsedCommands;
+    }
+
+    List<CommandOperation> commandsCopy = CommandOperation.clone(parsedCommands);
+
+    for (CommandOperation cmd : commandsCopy) {
+      JsonSchemaValidator validator = validators.get(cmd.name);
+      if (validator == null) {
+        cmd.addError(formatString("Unknown operation ''{0}'' available ops are ''{1}''", cmd.name,
+            validators.keySet()));
+        continue;
+      } else {
+        List<String> errs = validator.validateJson(cmd.getCommandData());
+        if (errs != null) for (String err : errs) cmd.addError(err);
+      }
+
+    }
+    List<Map> errs = CommandOperation.captureErrors(commandsCopy);
+    if (!errs.isEmpty()) {
+      throw new ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "Error in command payload", errs);
+    }
+    return commandsCopy;
+  }
+
+  public static class ExceptionWithErrObject extends SolrException {
+    private List<Map> errs;
+
+    public ExceptionWithErrObject(ErrorCode code, String msg, List<Map> errs) {
+      super(code, msg);
+      this.errs = errs;
+    }
+
+    public List<Map> getErrs() {
+      return errs;
+    }
+  }
+
+  public static class LazyLoadedApi extends Api {
+
+    private final PluginBag.PluginHolder<SolrRequestHandler> holder;
+    private Api delegate;
+
+    protected LazyLoadedApi(SpecProvider specProvider, PluginBag.PluginHolder<SolrRequestHandler> lazyPluginHolder) {
+      super(specProvider);
+      this.holder = lazyPluginHolder;
+    }
+
+    @Override
+    public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+      if (!holder.isLoaded()) {
+        delegate = new ReqHandlerToApi(holder.get(), ApiBag.EMPTY_SPEC);
+      }
+      delegate.call(req, rsp);
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/api/ApiSupport.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/api/ApiSupport.java b/solr/core/src/java/org/apache/solr/api/ApiSupport.java
new file mode 100644
index 0000000..ca1e866
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/ApiSupport.java
@@ -0,0 +1,46 @@
+/*
+ * 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.api;
+
+import java.util.Collection;
+
+/**The interface that is implemented by a request handler to support the V2 end point
+ *
+ */
+public interface ApiSupport {
+
+  /**It is possible to support multiple v2 apis by a single requesthandler
+   *
+   * @return the list of v2 api implementations
+   */
+  Collection<Api> getApis();
+
+  /**Whether this should be made available at the regular legacy path
+   */
+  default Boolean registerV1() {
+    return Boolean.TRUE;
+  }
+
+  /**Whether this request handler must be made available at the /v2/ path
+   */
+  default Boolean registerV2() {
+    return Boolean.FALSE;
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/api/SpecProvider.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/api/SpecProvider.java b/solr/core/src/java/org/apache/solr/api/SpecProvider.java
new file mode 100644
index 0000000..c373c99
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/SpecProvider.java
@@ -0,0 +1,25 @@
+
+/*
+ * 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.api;
+import org.apache.solr.common.util.ValidatingJsonMap;
+
+/**A generic interface for any class that is capable of providing its specification as a json schema
+ */
+public interface SpecProvider {
+  ValidatingJsonMap getSpec();
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
new file mode 100644
index 0000000..4a053dc
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -0,0 +1,340 @@
+/*
+ * 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.api;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.util.ValidatingJsonMap;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.PluginBag;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.logging.MDCLoggingContext;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestHandler;
+import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.AuthorizationContext;
+import org.apache.solr.servlet.HttpSolrCall;
+import org.apache.solr.servlet.SolrDispatchFilter;
+import org.apache.solr.servlet.SolrRequestParsers;
+import org.apache.solr.util.JsonSchemaValidator;
+import org.apache.solr.util.PathTrie;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.solr.common.params.CommonParams.JSON;
+import static org.apache.solr.common.params.CommonParams.WT;
+import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
+import static org.apache.solr.servlet.SolrDispatchFilter.Action.PASSTHROUGH;
+import static org.apache.solr.servlet.SolrDispatchFilter.Action.PROCESS;
+import static org.apache.solr.util.PathTrie.getPathSegments;
+
+// class that handle the '/v2' path
+public class V2HttpCall extends HttpSolrCall {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private Api api;
+  List<String> pieces;
+  private String prefix;
+  HashMap<String, String> parts = new HashMap<>();
+  static final Set<String> knownPrefixes = ImmutableSet.of("cluster", "node", "collections", "cores", "c");
+
+  public V2HttpCall(SolrDispatchFilter solrDispatchFilter, CoreContainer cc,
+                    HttpServletRequest request, HttpServletResponse response, boolean retry) {
+    super(solrDispatchFilter, cc, request, response, retry);
+  }
+
+  protected void init() throws Exception {
+    String path = this.path;
+    String fullPath = path = path.substring(3);//strip off '/v2'
+    try {
+      pieces = getPathSegments(path);
+      if (pieces.size() == 0) {
+        prefix = "c";
+        path = "/c";
+      } else {
+        prefix = pieces.get(0);
+      }
+
+      boolean isCompositeApi = false;
+      if (knownPrefixes.contains(prefix)) {
+        api = getApiInfo(cores.getRequestHandlers(), path, req.getMethod(), fullPath, parts);
+        if (api != null) {
+          isCompositeApi = api instanceof CompositeApi;
+          if (!isCompositeApi) {
+            initAdminRequest(path);
+            return;
+          }
+        }
+      }
+
+      if ("c".equals(prefix) || "collections".equals(prefix)) {
+        String collectionName = origCorename = corename = pieces.get(1);
+        DocCollection collection = getDocCollection(collectionName);
+        if (collection == null) {
+           if ( ! path.endsWith(ApiBag.INTROSPECT)) {
+            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "no such collection or alias");
+          }
+        } else {
+          boolean isPreferLeader = false;
+          if (path.endsWith("/update") || path.contains("/update/")) {
+            isPreferLeader = true;
+          }
+          core = getCoreByCollection(collection.getName(), isPreferLeader);
+          if (core == null) {
+            //this collection exists , but this node does not have a replica for that collection
+            //todo find a better way to compute remote
+            extractRemotePath(corename, origCorename, 0);
+            return;
+          }
+        }
+      } else if ("cores".equals(prefix)) {
+        origCorename = corename = pieces.get(1);
+        core = cores.getCore(corename);
+      }
+      if (core == null) {
+        log.error(">> path: '" + path + "'");
+        if (path.endsWith(ApiBag.INTROSPECT)) {
+          initAdminRequest(path);
+          return;
+        } else {
+          throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "no core retrieved for " + corename);
+        }
+      }
+
+      this.path = path = path.substring(prefix.length() + pieces.get(1).length() + 2);
+      Api apiInfo = getApiInfo(core.getRequestHandlers(), path, req.getMethod(), fullPath, parts);
+      if (isCompositeApi && apiInfo instanceof CompositeApi) {
+        ((CompositeApi) this.api).add(apiInfo);
+      } else {
+        api = apiInfo;
+      }
+      MDCLoggingContext.setCore(core);
+      parseRequest();
+
+      if (usingAliases) {
+        processAliases(aliases, collectionsList);
+      }
+
+      action = PROCESS;
+      // we are done with a valid handler
+    } catch (RuntimeException rte) {
+      log.error("Error in init()", rte);
+      throw rte;
+    } finally {
+      if (api == null) action = PASSTHROUGH;
+      if (solrReq != null) solrReq.getContext().put(CommonParams.PATH, path);
+    }
+  }
+
+  private void initAdminRequest(String path) throws Exception {
+    solrReq = SolrRequestParsers.DEFAULT.parse(null, path, req);
+    solrReq.getContext().put(CoreContainer.class.getName(), cores);
+    requestType = AuthorizationContext.RequestType.ADMIN;
+    action = ADMIN;
+  }
+
+  protected void parseRequest() throws Exception {
+    config = core.getSolrConfig();
+    // get or create/cache the parser for the core
+    SolrRequestParsers parser = config.getRequestParsers();
+
+    // With a valid handler and a valid core...
+
+    if (solrReq == null) solrReq = parser.parse(core, path, req);
+  }
+
+  protected DocCollection getDocCollection(String collectionName) {
+    if (!cores.isZooKeeperAware()) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Solr not running in cloud mode ");
+    }
+    ZkStateReader zkStateReader = cores.getZkController().getZkStateReader();
+    DocCollection collection = zkStateReader.getClusterState().getCollectionOrNull(collectionName);
+    if (collection == null) {
+      collectionName = corename = lookupAliases(collectionName);
+      collection = zkStateReader.getClusterState().getCollectionOrNull(collectionName);
+    }
+    return collection;
+  }
+
+  public static Api getApiInfo(PluginBag<SolrRequestHandler> requestHandlers,
+                               String path, String method,
+                               String fullPath,
+                               Map<String, String> parts) {
+    fullPath = fullPath == null ? path : fullPath;
+    Api api = requestHandlers.v2lookup(path, method, parts);
+    if (api == null && path.endsWith(ApiBag.INTROSPECT)) {
+      // the particular http method does not have any ,
+      // just try if any other method has this path
+      api = requestHandlers.v2lookup(path, null, parts);
+    }
+
+    if (api == null) {
+      return getSubPathApi(requestHandlers, path, fullPath, new CompositeApi(null));
+    }
+
+    if (api instanceof ApiBag.IntrospectApi) {
+      final Map<String, Api> apis = new LinkedHashMap<>();
+      for (String m : SolrRequest.SUPPORTED_METHODS) {
+        Api x = requestHandlers.v2lookup(path, m, parts);
+        if (x != null) apis.put(m, x);
+      }
+      api = new CompositeApi(new Api(ApiBag.EMPTY_SPEC) {
+        @Override
+        public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+          String method = req.getParams().get("method");
+          Set<Api> added = new HashSet<>();
+          for (Map.Entry<String, Api> e : apis.entrySet()) {
+            if (method == null || e.getKey().equals(method)) {
+              if (!added.contains(e.getValue())) {
+                e.getValue().call(req, rsp);
+                added.add(e.getValue());
+              }
+            }
+          }
+        }
+      });
+      getSubPathApi(requestHandlers,path, fullPath, (CompositeApi) api);
+    }
+
+
+    return api;
+  }
+
+  private static CompositeApi getSubPathApi(PluginBag<SolrRequestHandler> requestHandlers, String path, String fullPath, CompositeApi compositeApi) {
+
+    String newPath = path.endsWith(ApiBag.INTROSPECT) ? path.substring(0, path.length() - ApiBag.INTROSPECT.length()) : path;
+    Map<String, Set<String>> subpaths = new LinkedHashMap<>();
+
+    getSubPaths(newPath, requestHandlers.getApiBag(), subpaths);
+    final Map<String, Set<String>> subPaths = subpaths;
+    if (subPaths.isEmpty()) return null;
+    return compositeApi.add(new Api(() -> ValidatingJsonMap.EMPTY) {
+      @Override
+      public void call(SolrQueryRequest req1, SolrQueryResponse rsp) {
+        String prefix = null;
+        prefix = fullPath.endsWith(ApiBag.INTROSPECT) ?
+            fullPath.substring(0, fullPath.length() - ApiBag.INTROSPECT.length()) :
+            fullPath;
+        LinkedHashMap<String, Set<String>> result = new LinkedHashMap<>(subPaths.size());
+        for (Map.Entry<String, Set<String>> e : subPaths.entrySet()) {
+          if (e.getKey().endsWith(ApiBag.INTROSPECT)) continue;
+          result.put(prefix + e.getKey(), e.getValue());
+        }
+
+        Map m = (Map) rsp.getValues().get("availableSubPaths");
+        if(m != null){
+          m.putAll(result);
+        } else {
+          rsp.add("availableSubPaths", result);
+        }
+      }
+    });
+  }
+
+  private static void getSubPaths(String path, ApiBag bag, Map<String, Set<String>> pathsVsMethod) {
+    for (SolrRequest.METHOD m : SolrRequest.METHOD.values()) {
+      PathTrie<Api> registry = bag.getRegistry(m.toString());
+      if (registry != null) {
+        HashSet<String> subPaths = new HashSet<>();
+        registry.lookup(path, new HashMap<>(), subPaths);
+        for (String subPath : subPaths) {
+          Set<String> supportedMethods = pathsVsMethod.get(subPath);
+          if (supportedMethods == null) pathsVsMethod.put(subPath, supportedMethods = new HashSet<>());
+          supportedMethods.add(m.toString());
+        }
+      }
+    }
+  }
+
+  public static class CompositeApi extends Api {
+    private LinkedList<Api> apis = new LinkedList<>();
+
+    public CompositeApi(Api api) {
+      super(ApiBag.EMPTY_SPEC);
+      if (api != null) apis.add(api);
+    }
+
+    @Override
+    public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+      for (Api api : apis) {
+        api.call(req, rsp);
+      }
+
+    }
+
+    public CompositeApi add(Api api) {
+      apis.add(api);
+      return this;
+    }
+  }
+
+  @Override
+  protected void handleAdmin(SolrQueryResponse solrResp) {
+    api.call(this.solrReq, solrResp);
+  }
+
+  @Override
+  protected void execute(SolrQueryResponse rsp) {
+    try {
+      api.call(solrReq, rsp);
+    } catch (RuntimeException e) {
+      throw e;
+    }
+  }
+
+  @Override
+  protected Object _getHandler() {
+    return api;
+  }
+
+  public Map<String,String> getUrlParts(){
+    return parts;
+  }
+
+  @Override
+  protected QueryResponseWriter getResponseWriter() {
+    String wt = solrReq.getParams().get(WT, JSON);
+    if (core != null) return core.getResponseWriters().get(wt);
+    return SolrCore.DEFAULT_RESPONSE_WRITERS.get(wt);
+  }
+
+  @Override
+  protected ValidatingJsonMap getSpec() {
+    return api == null ? null : api.getSpec();
+  }
+
+  @Override
+  protected Map<String, JsonSchemaValidator> getValidators() {
+    return api == null ? null : api.getCommandSchema();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/api/package-info.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/api/package-info.java b/solr/core/src/java/org/apache/solr/api/package-info.java
new file mode 100644
index 0000000..c3574c7
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+/**
+ * Commonly used classes for Solr V2 API.
+ */
+package org.apache.solr.api;
+

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/cloud/Assign.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/Assign.java b/solr/core/src/java/org/apache/solr/cloud/Assign.java
index e6e08f9..ba03ccd 100644
--- a/solr/core/src/java/org/apache/solr/cloud/Assign.java
+++ b/solr/core/src/java/org/apache/solr/cloud/Assign.java
@@ -146,10 +146,16 @@ public class Assign {
   // could be created on live nodes given maxShardsPerNode, Replication factor (if from createShard) etc.
   public static List<ReplicaCount> getNodesForNewReplicas(ClusterState clusterState, String collectionName,
                                                           String shard, int numberOfNodes,
-                                                          String createNodeSetStr, CoreContainer cc) {
+                                                          Object createNodeSet, CoreContainer cc) {
     DocCollection coll = clusterState.getCollection(collectionName);
     Integer maxShardsPerNode = coll.getInt(MAX_SHARDS_PER_NODE, 1);
-    List<String> createNodeList = createNodeSetStr  == null ? null: StrUtils.splitSmart(createNodeSetStr, ",", true);
+    List<String> createNodeList = null;
+
+    if (createNodeSet instanceof List) {
+      createNodeList = (List) createNodeSet;
+    } else {
+      createNodeList = createNodeSet == null ? null : StrUtils.splitSmart((String) createNodeSet, ",", true);
+    }
 
      HashMap<String, ReplicaCount> nodeNameVsShardCount = getNodeNameVsShardCount(collectionName, clusterState, createNodeList);
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/cloud/CreateShardCmd.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/CreateShardCmd.java b/solr/core/src/java/org/apache/solr/cloud/CreateShardCmd.java
index 3d5aa41..52df32b 100644
--- a/solr/core/src/java/org/apache/solr/cloud/CreateShardCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/CreateShardCmd.java
@@ -68,7 +68,7 @@ public class CreateShardCmd implements Cmd {
     ShardHandler shardHandler = ocmh.shardHandlerFactory.getShardHandler();
     DocCollection collection = clusterState.getCollection(collectionName);
     int repFactor = message.getInt(REPLICATION_FACTOR, collection.getInt(REPLICATION_FACTOR, 1));
-    String createNodeSetStr = message.getStr(OverseerCollectionMessageHandler.CREATE_NODE_SET);
+    Object createNodeSetStr = message.get(OverseerCollectionMessageHandler.CREATE_NODE_SET);
     List<Assign.ReplicaCount> sortedNodeList = getNodesForNewReplicas(clusterState, collectionName, sliceName, repFactor,
         createNodeSetStr, ocmh.overseer.getZkController().getCoreContainer());
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/core/PluginBag.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/PluginBag.java b/solr/core/src/java/org/apache/solr/core/PluginBag.java
index 77e2379..ad8bdec 100644
--- a/solr/core/src/java/org/apache/solr/core/PluginBag.java
+++ b/solr/core/src/java/org/apache/solr/core/PluginBag.java
@@ -46,10 +46,15 @@ import org.apache.solr.util.SimplePostTool;
 import org.apache.solr.util.plugin.NamedListInitializedPlugin;
 import org.apache.solr.util.plugin.PluginInfoInitialized;
 import org.apache.solr.util.plugin.SolrCoreAware;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.api.ApiSupport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static java.util.Collections.singletonMap;
 import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.api.ApiBag.HANDLER_NAME;
 
 /**
  * This manages the lifecycle of a set of plugin of the same type .
@@ -63,11 +68,13 @@ public class PluginBag<T> implements AutoCloseable {
   private final Class klass;
   private SolrCore core;
   private final SolrConfig.SolrPluginInfo meta;
+  private final ApiBag apiBag;
 
   /**
    * Pass needThreadSafety=true if plugins can be added and removed concurrently with lookups.
    */
   public PluginBag(Class<T> klass, SolrCore core, boolean needThreadSafety) {
+    this.apiBag = klass == SolrRequestHandler.class ? new ApiBag(core != null) : null;
     this.core = core;
     this.klass = klass;
     // TODO: since reads will dominate writes, we could also think about creating a new instance of a map each time it changes.
@@ -174,16 +181,52 @@ public class PluginBag<T> implements AutoCloseable {
    */
   public T put(String name, T plugin) {
     if (plugin == null) return null;
-    PluginHolder<T> old = put(name, new PluginHolder<T>(null, plugin));
+    PluginHolder<T> pluginHolder = new PluginHolder<>(null, plugin);
+    pluginHolder.registerAPI = false;
+    PluginHolder<T> old = put(name, pluginHolder);
     return old == null ? null : old.get();
   }
 
-
   PluginHolder<T> put(String name, PluginHolder<T> plugin) {
-    PluginHolder<T> old = registry.put(name, plugin);
-    if (plugin.pluginInfo != null && plugin.pluginInfo.isDefault()) {
-      setDefault(name);
+    Boolean registerApi = null;
+    Boolean disableHandler = null;
+    if (plugin.pluginInfo != null) {
+      String registerAt = plugin.pluginInfo.attributes.get("registerPath");
+      if (registerAt != null) {
+        List<String> strs = StrUtils.splitSmart(registerAt, ',');
+        disableHandler = !strs.contains("/");
+        registerApi = strs.contains("/v2");
+      }
+    }
+
+    if (apiBag != null) {
+      if (plugin.isLoaded()) {
+        T inst = plugin.get();
+        if (inst instanceof ApiSupport) {
+          ApiSupport apiSupport = (ApiSupport) inst;
+          if (registerApi == null) registerApi = apiSupport.registerV2();
+          if (disableHandler == null) disableHandler = !apiSupport.registerV1();
+
+          if(registerApi) {
+            Collection<Api> apis = apiSupport.getApis();
+            if (apis != null) {
+              Map<String, String> nameSubstitutes = singletonMap(HANDLER_NAME, name);
+              for (Api api : apis) {
+                apiBag.register(api, nameSubstitutes);
+              }
+            }
+          }
+
+        }
+      } else {
+        if (registerApi != null && registerApi)
+          apiBag.registerLazy((PluginHolder<SolrRequestHandler>) plugin, plugin.pluginInfo);
+      }
     }
+    if(disableHandler == null) disableHandler = Boolean.FALSE;
+    PluginHolder<T> old = null;
+    if(!disableHandler) old = registry.put(name, plugin);
+    if (plugin.pluginInfo != null && plugin.pluginInfo.isDefault()) setDefault(name);
     if (plugin.isLoaded()) registerMBean(plugin.get(), core, name);
     return old;
   }
@@ -249,7 +292,7 @@ public class PluginBag<T> implements AutoCloseable {
     return result.isLoaded();
   }
 
-  private static void registerMBean(Object inst, SolrCore core, String pluginKey) {
+  private void registerMBean(Object inst, SolrCore core, String pluginKey) {
     if (core == null) return;
     if (inst instanceof SolrInfoMBean) {
       SolrInfoMBean mBean = (SolrInfoMBean) inst;
@@ -280,6 +323,7 @@ public class PluginBag<T> implements AutoCloseable {
   public static class PluginHolder<T> implements AutoCloseable {
     private T inst;
     protected final PluginInfo pluginInfo;
+    boolean registerAPI = false;
 
     public PluginHolder(PluginInfo info) {
       this.pluginInfo = info;
@@ -321,7 +365,7 @@ public class PluginBag<T> implements AutoCloseable {
    * A class that loads plugins Lazily. When the get() method is invoked
    * the Plugin is initialized and returned.
    */
-  public static class LazyPluginHolder<T> extends PluginHolder<T> {
+  public class LazyPluginHolder<T> extends PluginHolder<T> {
     private volatile T lazyInst;
     private final SolrConfig.SolrPluginInfo pluginMeta;
     protected SolrException solrException;
@@ -516,4 +560,17 @@ public class PluginBag<T> implements AutoCloseable {
       }
     }
   }
+
+
+  public Api v2lookup(String path, String method, Map<String, String> parts) {
+    if (apiBag == null) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "this should not happen, looking up for v2 API at the wrong place");
+    }
+    return apiBag.lookup(path, method, parts);
+  }
+
+  public ApiBag getApiBag() {
+    return apiBag;
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/BlobHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/BlobHandler.java b/solr/core/src/java/org/apache/solr/handler/BlobHandler.java
index 25b3b14..f5b49ea 100644
--- a/solr/core/src/java/org/apache/solr/handler/BlobHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/BlobHandler.java
@@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandles;
 import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -34,6 +35,8 @@ import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.TopDocs;
 import org.apache.lucene.search.TopFieldDocs;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrInputDocument;
 import org.apache.solr.common.params.CommonParams;
@@ -72,7 +75,7 @@ public class BlobHandler extends RequestHandlerBase implements PluginInfoInitial
 
   @Override
   public void handleRequestBody(final SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    String httpMethod = (String) req.getContext().get("httpMethod");
+    String httpMethod = req.getHttpMethod();
     String path = (String) req.getContext().get("path");
     SolrConfigHandler.setWt(req, JSON);
 
@@ -277,4 +280,13 @@ public class BlobHandler extends RequestHandlerBase implements PluginInfoInitial
     req.getCore().getRequestHandler(handler).handleRequest(r, rsp);
   }
 
+  @Override
+  public Boolean registerV2() {
+    return Boolean.TRUE;
+  }
+
+  @Override
+  public Collection<Api> getApis() {
+    return ApiBag.wrapRequestHandlers(this, "core.system.blob", "core.system.blob.upload");
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/DumpRequestHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/DumpRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/DumpRequestHandler.java
index ecafb52..d7d5b71 100644
--- a/solr/core/src/java/org/apache/solr/handler/DumpRequestHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/DumpRequestHandler.java
@@ -19,7 +19,9 @@ package org.apache.solr.handler;
 import java.io.IOException;
 import java.io.Reader;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.solr.common.util.ContentStream;
@@ -39,6 +41,15 @@ public class DumpRequestHandler extends RequestHandlerBase
   {
     // Show params
     rsp.add( "params", req.getParams().toNamedList() );
+    String[] parts = req.getParams().getParams("urlTemplateValues");
+    if (parts != null && parts.length > 0) {
+      Map map = new LinkedHashMap<>();
+      rsp.getValues().add("urlTemplateValues", map);
+      for (String part : parts) {
+        map.put(part, req.getPathTemplateValues().get(part));
+      }
+    }
+
     String[] returnParams = req.getParams().getParams("param");
     if(returnParams !=null) {
       NamedList params = (NamedList) rsp.getValues().get("params");

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/PingRequestHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/PingRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/PingRequestHandler.java
index 04b930a..8230bf5 100644
--- a/solr/core/src/java/org/apache/solr/handler/PingRequestHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/PingRequestHandler.java
@@ -330,6 +330,11 @@ public class PingRequestHandler extends RequestHandlerBase implements SolrCoreAw
   }
 
   @Override
+  public Boolean registerV2() {
+    return Boolean.TRUE;
+  }
+
+  @Override
   public Category getCategory() {
     return Category.ADMIN;
   }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/RealTimeGetHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/RealTimeGetHandler.java b/solr/core/src/java/org/apache/solr/handler/RealTimeGetHandler.java
index 6c9b0a9..9049318 100644
--- a/solr/core/src/java/org/apache/solr/handler/RealTimeGetHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/RealTimeGetHandler.java
@@ -16,12 +16,16 @@
  */
 package org.apache.solr.handler;
 
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
 import org.apache.solr.handler.component.*;
 
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
+
 public class RealTimeGetHandler extends SearchHandler {
   @Override
   protected List<String> getDefaultComponents()
@@ -42,6 +46,16 @@ public class RealTimeGetHandler extends SearchHandler {
   public URL[] getDocs() {
     return null;
   }
+
+  @Override
+  public Collection<Api> getApis() {
+    return ApiBag.wrapRequestHandlers(this, "core.RealtimeGet");
+  }
+
+  @Override
+  public Boolean registerV2() {
+    return Boolean.TRUE;
+  }
 }
 
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
index b70c096..3c6f5fa 100644
--- a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
@@ -18,7 +18,9 @@ package org.apache.solr.handler;
 
 import java.lang.invoke.MethodHandles;
 import java.net.URL;
+import java.util.Collection;
 
+import com.google.common.collect.ImmutableList;
 import com.codahale.metrics.Counter;
 import com.codahale.metrics.Meter;
 import com.codahale.metrics.Timer;
@@ -37,6 +39,9 @@ import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.search.SyntaxError;
 import org.apache.solr.util.SolrPluginUtils;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.api.ApiSupport;
 import org.apache.solr.util.stats.MetricUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -46,7 +51,7 @@ import static org.apache.solr.core.RequestParams.USEPARAM;
 /**
  *
  */
-public abstract class RequestHandlerBase implements SolrRequestHandler, SolrInfoMBean, SolrMetricProducer, NestedRequestHandler {
+public abstract class RequestHandlerBase implements SolrRequestHandler, SolrInfoMBean, SolrMetricProducer, NestedRequestHandler,ApiSupport {
 
   protected NamedList initArgs = null;
   protected SolrParams defaults;
@@ -290,6 +295,11 @@ public abstract class RequestHandlerBase implements SolrRequestHandler, SolrInfo
     MetricUtils.addMetrics(lst, requestTimes);
     return lst;
   }
+
+  @Override
+  public Collection<Api> getApis() {
+    return ImmutableList.of(new ApiBag.ReqHandlerToApi(this, ApiBag.constructSpec(pluginInfo)));
+  }
 }
 
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
index 9c2d45c..f3e503e 100644
--- a/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SchemaHandler.java
@@ -19,17 +19,19 @@ package org.apache.solr.handler;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
 import org.apache.solr.cloud.ZkSolrResourceLoader;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.ContentStream;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
@@ -86,15 +88,12 @@ public class SchemaHandler extends RequestHandlerBase implements SolrCoreAware,
         return;
       }
 
-      for (ContentStream stream : req.getContentStreams()) {
-        try {
-          List errs = new SchemaManager(req).performOperations(stream.getReader());
-          if (!errs.isEmpty()) rsp.add("errors", errs);
-        } catch (IOException e) {
-          rsp.add("errors", Collections.singletonList("Error reading input String " + e.getMessage()));
-          rsp.setException(e);
-        }
-        break;
+      try {
+        List errs = new SchemaManager(req).performOperations();
+        if (!errs.isEmpty()) rsp.add("errors", errs);
+      } catch (IOException e) {
+        rsp.add("errors", Collections.singletonList("Error reading input String " + e.getMessage()));
+        rsp.setException(e);
       }
     } else {
       handleGET(req, rsp);
@@ -260,4 +259,20 @@ public class SchemaHandler extends RequestHandlerBase implements SolrCoreAware,
   public void inform(SolrCore core) {
     isImmutableConfigSet = SolrConfigHandler.getImmutable(core);
   }
+
+  @Override
+  public Collection<Api> getApis() {
+    return ApiBag.wrapRequestHandlers(this, "core.SchemaRead",
+        "core.SchemaRead.fields",
+        "core.SchemaRead.copyFields",
+        "core.SchemaEdit",
+        "core.SchemaRead.dynamicFields_fieldTypes"
+        );
+
+  }
+
+  @Override
+  public Boolean registerV2() {
+    return Boolean.TRUE;
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index 1c584b1..2660cba 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -76,6 +76,8 @@ import org.apache.solr.util.DefaultSolrThreadFactory;
 import org.apache.solr.util.RTimer;
 import org.apache.solr.util.SolrPluginUtils;
 import org.apache.solr.util.plugin.SolrCoreAware;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -895,4 +897,18 @@ public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAwa
       return null;
     }
   }
+
+  @Override
+  public Collection<Api> getApis() {
+    return ApiBag.wrapRequestHandlers(this,
+        "core.config",
+        "core.config.Commands",
+        "core.config.Params",
+        "core.config.Params.Commands");
+  }
+
+  @Override
+  public Boolean registerV2() {
+    return Boolean.TRUE;
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java b/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java
index 6628368..fd7a754 100644
--- a/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandler.java
@@ -150,6 +150,7 @@ public class UpdateRequestHandler extends ContentStreamHandlerBase implements Pe
     pathVsLoaders.put(JSON_PATH,registry.get("application/json"));
     pathVsLoaders.put(DOC_PATH,registry.get("application/json"));
     pathVsLoaders.put(CSV_PATH,registry.get("application/csv"));
+    pathVsLoaders.put(BIN_PATH,registry.get("application/javabin"));
     return registry;
   }
 
@@ -178,6 +179,7 @@ public class UpdateRequestHandler extends ContentStreamHandlerBase implements Pe
   public static final String DOC_PATH = "/update/json/docs";
   public static final String JSON_PATH = "/update/json";
   public static final String CSV_PATH = "/update/csv";
+  public static final String BIN_PATH = "/update/bin";
 
 }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandlerApi.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandlerApi.java b/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandlerApi.java
new file mode 100644
index 0000000..6ba3229
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/UpdateRequestHandlerApi.java
@@ -0,0 +1,73 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+
+public class UpdateRequestHandlerApi extends UpdateRequestHandler  {
+
+
+  @Override
+  public Collection<Api> getApis() {
+    return Collections.singleton(getApiImpl());
+  }
+
+  private Api getApiImpl() {
+    return new Api(ApiBag.getSpec("core.Update")) {
+      @Override
+      public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+        String path = req.getPath();
+        String target =  mapping.get(path);
+        if(target != null) req.getContext().put("path", target);
+        try {
+          handleRequest(req, rsp);
+        } catch (RuntimeException e) {
+          throw e;
+        } catch (Exception e){
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,e );
+        }
+      }
+    };
+  }
+
+  @Override
+  public Boolean registerV1() {
+    return Boolean.FALSE;
+  }
+
+  @Override
+  public Boolean registerV2() {
+    return Boolean.TRUE;
+  }
+
+  private static final Map<String, String> mapping = ImmutableMap.<String,String>builder()
+      .put("/update", DOC_PATH)
+      .put(JSON_PATH, DOC_PATH)
+      .put("/update/json/commands", JSON_PATH)
+      .build();
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java b/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java
new file mode 100644
index 0000000..0e58ccc
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java
@@ -0,0 +1,236 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.CommandOperation;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.ApiBag;
+import org.apache.solr.api.ApiSupport;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.common.SolrException.ErrorCode.BAD_REQUEST;
+import static org.apache.solr.common.util.StrUtils.splitSmart;
+
+/**
+ * This is a utility class to provide an easy mapping of request handlers which support multiple commands
+ * to the V2 API format (core admin api, collections api). This helps in automatically mapping paths
+ * to actions and old parameter names to new parameter names
+ */
+public abstract class BaseHandlerApiSupport implements ApiSupport {
+  protected final Map<SolrRequest.METHOD, Map<V2EndPoint, List<ApiCommand>>> commandsMapping;
+
+  protected BaseHandlerApiSupport() {
+    commandsMapping = new HashMap<>();
+    for (ApiCommand cmd : getCommands()) {
+      Map<V2EndPoint, List<ApiCommand>> m = commandsMapping.get(cmd.getHttpMethod());
+      if (m == null) commandsMapping.put(cmd.getHttpMethod(), m = new HashMap<>());
+      List<ApiCommand> list = m.get(cmd.getEndPoint());
+      if (list == null) m.put(cmd.getEndPoint(), list = new ArrayList<>());
+      list.add(cmd);
+    }
+  }
+
+  @Override
+  public synchronized Collection<Api> getApis() {
+    ImmutableList.Builder<Api> l = ImmutableList.builder();
+    for (V2EndPoint op : getEndPoints()) l.add(getApi(op));
+    return l.build();
+  }
+
+
+  private Api getApi(final V2EndPoint op) {
+    final BaseHandlerApiSupport apiHandler = this;
+    return new Api(ApiBag.getSpec(op.getSpecName())) {
+      @Override
+      public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+        SolrParams params = req.getParams();
+        SolrRequest.METHOD method = SolrRequest.METHOD.valueOf(req.getHttpMethod());
+        List<ApiCommand> commands = commandsMapping.get(method).get(op);
+        try {
+          if (method == POST) {
+            List<CommandOperation> cmds = req.getCommands(true);
+            if (cmds.size() > 1)
+              throw new SolrException(BAD_REQUEST, "Only one command is allowed");
+            CommandOperation c = cmds.size() == 0 ? null : cmds.get(0);
+            ApiCommand command = null;
+            String commandName = c == null ? null : c.name;
+            for (ApiCommand cmd : commands) {
+              if (Objects.equals(cmd.getName(), commandName)) {
+                command = cmd;
+                break;
+              }
+            }
+
+            if (command == null) {
+              throw new SolrException(BAD_REQUEST, " no such command " + c);
+            }
+            wrapParams(req, c, command, false);
+            command.invoke(req, rsp, apiHandler);
+
+          } else {
+            if (commands == null || commands.isEmpty()) {
+              rsp.add("error", "No support for : " + method + " at :" + req.getPath());
+              return;
+            }
+            if (commands.size() > 1) {
+              for (ApiCommand command : commands) {
+                if (command.getName().equals(req.getPath())) {
+                  commands = Collections.singletonList(command);
+                  break;
+                }
+              }
+            }
+            wrapParams(req, new CommandOperation("", Collections.EMPTY_MAP), commands.get(0), true);
+            commands.get(0).invoke(req, rsp, apiHandler);
+          }
+
+        } catch (SolrException e) {
+          throw e;
+        } catch (Exception e) {
+          throw new SolrException(BAD_REQUEST, e);
+        } finally {
+          req.setParams(params);
+        }
+
+      }
+    };
+
+  }
+
+  private static void wrapParams(final SolrQueryRequest req, final CommandOperation co, final ApiCommand cmd, final boolean useRequestParams) {
+    final Map<String, String> pathValues = req.getPathTemplateValues();
+    final Map<String, Object> map = co == null || !(co.getCommandData() instanceof Map) ?
+        Collections.singletonMap("", co.getCommandData()) : co.getDataMap();
+    final SolrParams origParams = req.getParams();
+
+    req.setParams(
+        new SolrParams() {
+          @Override
+          public String get(String param) {
+            Object vals = getParams0(param);
+            if (vals == null) return null;
+            if (vals instanceof String) return (String) vals;
+            if (vals instanceof Boolean || vals instanceof Number) return String.valueOf(vals);
+            if (vals instanceof String[] && ((String[]) vals).length > 0) return ((String[]) vals)[0];
+            return null;
+          }
+
+          private Object getParams0(String param) {
+            param = cmd.getParamSubstitute(param);
+            Object o = param.indexOf('.') > 0 ?
+                Utils.getObjectByPath(map, true, splitSmart(param, '.')) :
+                map.get(param);
+            if (o == null) o = pathValues.get(param);
+            if (o == null && useRequestParams) o = origParams.getParams(param);
+            if (o instanceof List) {
+              List l = (List) o;
+              return l.toArray(new String[l.size()]);
+            }
+
+            return o;
+          }
+
+          @Override
+          public String[] getParams(String param) {
+            Object vals = getParams0(param);
+            return vals == null || vals instanceof String[] ?
+                (String[]) vals :
+                new String[]{vals.toString()};
+          }
+
+          @Override
+          public Iterator<String> getParameterNamesIterator() {
+            return cmd.getParamNames(co).iterator();
+
+          }
+
+
+        });
+
+  }
+
+
+  public static Collection<String> getParamNames(CommandOperation op, ApiCommand command) {
+    List<String> result = new ArrayList<>();
+    Object o = op.getCommandData();
+    if (o instanceof Map) {
+      Map map = (Map) o;
+      collectKeyNames(map, result, "");
+    }
+    return result;
+
+  }
+
+  public static void collectKeyNames(Map<String, Object> map, List<String> result, String prefix) {
+    for (Map.Entry<String, Object> e : map.entrySet()) {
+      if (e.getValue() instanceof Map) {
+        collectKeyNames((Map) e.getValue(), result, prefix + e.getKey() + ".");
+      } else {
+        result.add(prefix + e.getKey());
+      }
+    }
+  }
+
+  protected abstract List<ApiCommand> getCommands();
+
+  protected abstract List<V2EndPoint> getEndPoints();
+
+
+  public interface ApiCommand {
+    String getName();
+
+    /**
+     * the http method supported by this command
+     */
+    SolrRequest.METHOD getHttpMethod();
+
+    V2EndPoint getEndPoint();
+
+    default Collection<String> getParamNames(CommandOperation op) {
+      return BaseHandlerApiSupport.getParamNames(op, this);
+    }
+
+
+    default String getParamSubstitute(String name) {
+      return name;
+    }
+
+    void invoke(SolrQueryRequest req, SolrQueryResponse rsp, BaseHandlerApiSupport apiHandler) throws Exception;
+  }
+
+  public interface V2EndPoint {
+
+    String getSpecName();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/71abe130/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java
new file mode 100644
index 0000000..581fe46
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java
@@ -0,0 +1,319 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.handler.admin.CollectionsHandler.CollectionOperation;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.CommandOperation;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.cloud.OverseerCollectionMessageHandler.COLL_CONF;
+import static org.apache.solr.cloud.OverseerCollectionMessageHandler.CREATE_NODE_SET;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.handler.admin.CollectionsHandler.CollectionOperation.*;
+
+
+public class CollectionHandlerApi extends BaseHandlerApiSupport {
+  final CollectionsHandler handler;
+
+  public CollectionHandlerApi(CollectionsHandler handler) {
+    this.handler = handler;
+  }
+
+  @Override
+  protected List<ApiCommand> getCommands() {
+    return Arrays.asList(Cmd.values());
+  }
+
+  @Override
+  protected List<V2EndPoint> getEndPoints() {
+    return Arrays.asList(EndPoint.values());
+  }
+
+
+  enum Cmd implements ApiCommand {
+    GET_COLLECTIONS(EndPoint.COLLECTIONS, GET, LIST_OP),
+    GET_CLUSTER(EndPoint.CLUSTER, GET, LIST_OP, "/cluster", null),
+    GET_CLUSTER_OVERSEER(EndPoint.CLUSTER, GET, OVERSEERSTATUS_OP, "/cluster/overseer", null),
+    GET_CLUSTER_STATUS_CMD(EndPoint.CLUSTER_CMD_STATUS, GET, REQUESTSTATUS_OP),
+    DELETE_CLUSTER_STATUS(EndPoint.CLUSTER_CMD_STATUS_DELETE, DELETE, DELETESTATUS_OP),
+    GET_A_COLLECTION(EndPoint.COLLECTION_STATE, GET, CLUSTERSTATUS_OP),
+    CREATE_COLLECTION(EndPoint.COLLECTIONS_COMMANDS,
+        POST,
+        CREATE_OP,
+        CREATE_OP.action.toLower(),
+        ImmutableMap.of(
+            COLL_CONF, "config",
+            "createNodeSet.shuffle", "shuffleNodes",
+            "createNodeSet", "nodeSet"
+        ),
+        ImmutableMap.of("properties.", "property.")),
+
+    DELETE_COLL(EndPoint.PER_COLLECTION_DELETE,
+        DELETE,
+        DELETE_OP,
+        DELETE_OP.action.toLower(),
+        ImmutableMap.of(NAME, "collection")),
+
+    RELOAD_COLL(EndPoint.PER_COLLECTION,
+        POST,
+        RELOAD_OP,
+        RELOAD_OP.action.toLower(),
+        ImmutableMap.of(NAME, "collection")),
+    MODIFYCOLLECTION(EndPoint.PER_COLLECTION,
+        POST,
+        MODIFYCOLLECTION_OP,
+        "modify",null),
+    MIGRATE_DOCS(EndPoint.PER_COLLECTION,
+        POST,
+        MIGRATE_OP,
+        "migrate-docs",
+        ImmutableMap.of("split.key", "splitKey",
+            "target.collection", "target",
+            "forward.timeout", "forwardTimeout"
+        )),
+    REBALANCELEADERS(EndPoint.PER_COLLECTION,
+        POST,
+        REBALANCELEADERS_OP,
+        "rebalance-leaders", null),
+    CREATE_ALIAS(EndPoint.COLLECTIONS_COMMANDS,
+        POST,
+        CREATEALIAS_OP,
+        "create-alias",
+        null),
+
+    DELETE_ALIAS(EndPoint.COLLECTIONS_COMMANDS,
+        POST,
+        DELETEALIAS_OP,
+        "delete-alias",
+        null),
+    CREATE_SHARD(EndPoint.PER_COLLECTION_SHARDS_COMMANDS,
+        POST,
+        CREATESHARD_OP,
+        "create",
+        ImmutableMap.of(CREATE_NODE_SET, "nodeSet"),
+        ImmutableMap.of("coreProperties.", "property.")) {
+      @Override
+      public String getParamSubstitute(String param) {
+        return super.getParamSubstitute(param);
+      }
+    },
+
+    SPLIT_SHARD(EndPoint.PER_COLLECTION_SHARDS_COMMANDS,
+        POST,
+        SPLITSHARD_OP,
+        "split",
+        ImmutableMap.of(
+            "split.key", "splitKey"),
+        ImmutableMap.of("coreProperties.", "property.")),
+    DELETE_SHARD(EndPoint.PER_COLLECTION_PER_SHARD_DELETE,
+        DELETE,
+        DELETESHARD_OP),
+
+    CREATE_REPLICA(EndPoint.PER_COLLECTION_SHARDS_COMMANDS,
+        POST,
+        ADDREPLICA_OP,
+        "add-replica",
+        null,
+        ImmutableMap.of("coreProperties.", "property.")),
+
+    DELETE_REPLICA(EndPoint.PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE,
+        DELETE,
+        DELETEREPLICA_OP),
+
+    SYNC_SHARD(EndPoint.PER_COLLECTION_PER_SHARD_COMMANDS,
+        POST,
+        SYNCSHARD_OP,
+        "synch-shard",
+        null),
+    ADDREPLICAPROP(EndPoint.PER_COLLECTION,
+        POST,
+        ADDREPLICAPROP_OP,
+        "add-replica-property",
+        ImmutableMap.of("property", "name", "property.value", "value")),
+    DELETEREPLICAPROP(EndPoint.PER_COLLECTION,
+        POST,
+        DELETEREPLICAPROP_OP,
+        "delete-replica-property",
+        null),
+    ADDROLE(EndPoint.CLUSTER_CMD,
+        POST,
+        ADDROLE_OP,
+        "add-role",null),
+    REMOVEROLE(EndPoint.CLUSTER_CMD,
+        POST,
+        REMOVEROLE_OP,
+        "remove-role",null),
+
+    CLUSTERPROP(EndPoint.CLUSTER_CMD,
+        POST,
+        CLUSTERPROP_OP,
+        "set-property",null),
+
+    BACKUP(EndPoint.COLLECTIONS_COMMANDS,
+        POST,
+        BACKUP_OP,
+        "backup-collection", null
+        ),
+    RESTORE(EndPoint.COLLECTIONS_COMMANDS,
+        POST,
+        RESTORE_OP,
+        "restore-collection",
+        null
+    ),
+    GET_NODES(EndPoint.CLUSTER_NODES, GET, null) {
+      @Override
+      public void invoke(SolrQueryRequest req, SolrQueryResponse rsp, BaseHandlerApiSupport apiHandler) throws Exception {
+        rsp.add("nodes", ((CollectionHandlerApi) apiHandler).handler.coreContainer.getZkController().getClusterState().getLiveNodes());
+      }
+    },
+    FORCELEADER(EndPoint.PER_COLLECTION_PER_SHARD_COMMANDS,POST, FORCELEADER_OP,"force-leader",null),
+    SYNCSHARD(EndPoint.PER_COLLECTION_PER_SHARD_COMMANDS,POST, SYNCSHARD_OP, "sync-shard",null),
+    BALANCESHARDUNIQUE(EndPoint.PER_COLLECTION, POST, BALANCESHARDUNIQUE_OP, "balance-shard-unique",null)
+
+    ;
+    public final String commandName;
+    public final EndPoint endPoint;
+    public final SolrRequest.METHOD method;
+    public final CollectionOperation target;
+    //mapping of http param name to json attribute
+    public final Map<String, String> paramstoAttr;
+    //mapping of old prefix to new for instance properties.a=val can be substituted with property:{a:val}
+    public final Map<String, String> prefixSubstitutes;
+
+    public SolrRequest.METHOD getMethod() {
+      return method;
+    }
+
+
+    Cmd(EndPoint endPoint, SolrRequest.METHOD method, CollectionOperation target) {
+      this(endPoint, method, target, null, null);
+    }
+
+    Cmd(EndPoint endPoint, SolrRequest.METHOD method, CollectionOperation target,
+        String commandName, Map<String, String> paramstoAttr) {
+      this(endPoint, method, target, commandName, paramstoAttr, Collections.EMPTY_MAP);
+
+    }
+
+    Cmd(EndPoint endPoint, SolrRequest.METHOD method, CollectionOperation target,
+        String commandName, Map<String, String> paramstoAttr, Map<String, String> prefixSubstitutes) {
+      this.commandName = commandName;
+      this.endPoint = endPoint;
+      this.method = method;
+      this.target = target;
+      this.paramstoAttr = paramstoAttr == null ? Collections.EMPTY_MAP : paramstoAttr;
+      this.prefixSubstitutes = prefixSubstitutes;
+
+    }
+
+    @Override
+    public String getName() {
+      return commandName;
+    }
+
+    @Override
+    public SolrRequest.METHOD getHttpMethod() {
+      return method;
+    }
+
+    @Override
+    public V2EndPoint getEndPoint() {
+      return endPoint;
+    }
+
+
+    @Override
+    public Collection<String> getParamNames(CommandOperation op) {
+      Collection<String> paramNames = BaseHandlerApiSupport.getParamNames(op, this);
+      if (!prefixSubstitutes.isEmpty()) {
+        Collection<String> result = new ArrayList<>(paramNames.size());
+        for (Map.Entry<String, String> e : prefixSubstitutes.entrySet()) {
+          for (String paramName : paramNames) {
+            if (paramName.startsWith(e.getKey())) {
+              result.add(paramName.replace(e.getKey(), e.getValue()));
+            } else {
+              result.add(paramName);
+            }
+          }
+          paramNames = result;
+        }
+      }
+
+      return paramNames;
+    }
+
+    @Override
+    public String getParamSubstitute(String param) {
+      String s = paramstoAttr.containsKey(param) ? paramstoAttr.get(param) : param;
+      if (prefixSubstitutes != null) {
+        for (Map.Entry<String, String> e : prefixSubstitutes.entrySet()) {
+          if (s.startsWith(e.getValue())) return s.replace(e.getValue(), e.getKey());
+        }
+      }
+      return s;
+    }
+
+    public void invoke(SolrQueryRequest req, SolrQueryResponse rsp, BaseHandlerApiSupport apiHandler)
+        throws Exception {
+      ((CollectionHandlerApi) apiHandler).handler.invokeAction(req, rsp, ((CollectionHandlerApi) apiHandler).handler.coreContainer, target.action, target);
+    }
+
+  }
+
+  enum EndPoint implements V2EndPoint {
+    CLUSTER("cluster"),
+    CLUSTER_CMD("cluster.Commands"),
+    CLUSTER_NODES("cluster.nodes"),
+    CLUSTER_CMD_STATUS("cluster.commandstatus"),
+    CLUSTER_CMD_STATUS_DELETE("cluster.commandstatus.delete"),
+    COLLECTIONS_COMMANDS("collections.Commands"),
+    COLLECTIONS("collections"),
+    COLLECTION_STATE("collections.collection"),
+    PER_COLLECTION("collections.collection.Commands"),
+    PER_COLLECTION_DELETE("collections.collection.delete"),
+    PER_COLLECTION_SHARDS_COMMANDS("collections.collection.shards.Commands"),
+    PER_COLLECTION_PER_SHARD_COMMANDS("collections.collection.shards.shard.Commands"),
+    PER_COLLECTION_PER_SHARD_DELETE("collections.collection.shards.shard.delete"),
+    PER_COLLECTION_PER_SHARD_PER_REPLICA_DELETE("collections.collection.shards.shard.replica.delete");
+    final String specName;
+
+
+    EndPoint(String specName) {
+      this.specName = specName;
+    }
+
+    @Override
+    public String getSpecName() {
+      return specName;
+    }
+  }
+
+}