blob: 5eae502e9e90c46ac28a3a3ecfaac304a62d5f84 [file] [log] [blame]
/*
* 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 com.google.common.collect.ImmutableSet;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.SolrThreadSafe;
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.JsonSchemaValidator;
import org.apache.solr.common.util.PathTrie;
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.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.servlet.HttpSolrCall;
import org.apache.solr.servlet.SolrDispatchFilter;
import org.apache.solr.servlet.SolrRequestParsers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.invoke.MethodHandles;
import java.util.*;
import java.util.function.Supplier;
import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.common.util.PathTrie.getPathSegments;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.*;
// class that handle the '/v2' path
@SolrThreadSafe
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;
final String fullPath = path = path.substring(7);//strip off '/____v2'
try {
pieces = getPathSegments(path);
if (pieces.size() == 0 || (pieces.size() == 1 && path.endsWith(CommonParams.INTROSPECT))) {
api = new Api(null) {
@Override
public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
rsp.add("documentation", "https://lucene.apache.org/solr/guide/v2-api.html");
rsp.add("description", "V2 API root path");
}
};
initAdminRequest(path);
return;
} 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)) {
origCorename = pieces.get(1);
DocCollection collection = resolveDocCollection(queryParams.get(COLLECTION_PROP, origCorename));
if (collection == null) {
if ( ! path.endsWith(CommonParams.INTROSPECT)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "no such collection or alias");
}
} else {
boolean isPreferLeader = (path.endsWith("/update") || path.contains("/update/"));
core = getCoreByCollection(collection.getName(), isPreferLeader);
if (core == null) {
//this collection exists , but this node does not have a replica for that collection
extractRemotePath(collection.getName(), collection.getName());
if (action == REMOTEQUERY) {
coreUrl = coreUrl.replace("/solr/", "/solr/____v2/c/");
this.path = path = path.substring(prefix.length() + collection.getName().length() + 2);
return;
}
}
}
} else if ("cores".equals(prefix)) {
origCorename = pieces.get(1);
core = cores.getCore(origCorename);
} else {
api = getApiInfo(cores.getRequestHandlers(), path, req.getMethod(), fullPath, parts);
if(api != null) {
//custom plugin
initAdminRequest(path);
return;
}
}
if (core == null) {
log.error(">> path: '{}'", path);
if (path.endsWith(CommonParams.INTROSPECT)) {
initAdminRequest(path);
return;
} else {
throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "no core retrieved for " + origCorename);
}
}
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 == null ? api : apiInfo;
}
parseRequest();
addCollectionParamIfNeeded(getCollectionsList());
action = PROCESS;
// we are done with a valid handler
} catch (RuntimeException rte) {
log.error("Error in init()", rte);
throw rte;
} finally {
if (action == null && api == null) action = PROCESS;
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);
}
/**
* Lookup the collection from the collection string (maybe comma delimited).
* Also sets {@link #collectionsList} by side-effect.
* if {@code secondTry} is false then we'll potentially recursively try this all one more time while ensuring
* the alias and collection info is sync'ed from ZK.
*/
protected DocCollection resolveDocCollection(String collectionStr) {
if (!cores.isZooKeeperAware()) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Solr not running in cloud mode ");
}
ZkStateReader zkStateReader = cores.getZkController().getZkStateReader();
Supplier<DocCollection> logic = () -> {
this.collectionsList = resolveCollectionListOrAlias(collectionStr); // side-effect
if (collectionsList.size() > 1) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Request must be sent to a single collection " +
"or an alias that points to a single collection," +
" but '" + collectionStr + "' resolves to " + this.collectionsList);
}
String collectionName = collectionsList.get(0); // first
//TODO an option to choose another collection in the list if can't find a local replica of the first?
return zkStateReader.getClusterState().getCollectionOrNull(collectionName);
};
DocCollection docCollection = logic.get();
if (docCollection != null) {
return docCollection;
}
// ensure our view is up to date before trying again
try {
zkStateReader.aliasesManager.update();
zkStateReader.forceUpdateCollection(collectionsList.get(0));
} catch (Exception e) {
log.error("Error trying to update state while resolving collection.", e);
//don't propagate exception on purpose
}
return logic.get();
}
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(CommonParams.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());
}
}
}
RequestHandlerUtils.addExperimentalFormatWarning(rsp);
}
});
getSubPathApi(requestHandlers,path, fullPath, (CompositeApi) api);
}
return api;
}
@SuppressWarnings({"unchecked"})
private static CompositeApi getSubPathApi(PluginBag<SolrRequestHandler> requestHandlers, String path, String fullPath, CompositeApi compositeApi) {
String newPath = path.endsWith(CommonParams.INTROSPECT) ? path.substring(0, path.length() - CommonParams.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(CommonParams.INTROSPECT) ?
fullPath.substring(0, fullPath.length() - CommonParams.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(CommonParams.INTROSPECT)) continue;
result.put(prefix + e.getKey(), e.getValue());
}
@SuppressWarnings({"rawtypes"})
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) {
try {
api.call(this.solrReq, solrResp);
} catch (Exception e) {
solrResp.setException(e);
}
}
@Override
protected void execute(SolrQueryResponse rsp) {
SolrCore.preDecorateResponse(solrReq, rsp);
if (api == null) {
rsp.setException(new SolrException(SolrException.ErrorCode.NOT_FOUND,
"Cannot find correspond api for the path : " + solrReq.getContext().get(CommonParams.PATH)));
} else {
try {
api.call(solrReq, rsp);
} catch (Exception e) {
rsp.setException(e);
}
}
SolrCore.postDecorateResponse(handler, solrReq, rsp);
}
@Override
protected Object _getHandler() {
return api;
}
public Map<String,String> getUrlParts(){
return parts;
}
@Override
protected ValidatingJsonMap getSpec() {
return api == null ? null : api.getSpec();
}
@Override
protected Map<String, JsonSchemaValidator> getValidators() {
return api == null ? null : api.getCommandSchema();
}
}