blob: 6912af395103456746dcd2554c0ef35d54b1c66b [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 static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROP;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.ADMIN_OR_REMOTEQUERY;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.PROCESS;
import static org.apache.solr.servlet.SolrDispatchFilter.Action.REMOTEQUERY;
import io.opentelemetry.api.trace.Span;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.jcip.annotations.ThreadSafe;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrException;
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.SuppressForbidden;
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.jersey.RequestContextKeys;
import org.apache.solr.jersey.container.ContainerRequestUtils;
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.servlet.cache.Method;
import org.apache.solr.util.tracing.TraceUtils;
import org.glassfish.jersey.server.ApplicationHandler;
import org.glassfish.jersey.server.ContainerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// class that handle the '/v2' path
@ThreadSafe
public class V2HttpCall extends HttpSolrCall {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private Api api;
private List<String> pathSegments;
private String prefix;
private boolean servedByJaxRs =
false; // A flag indicating whether the request was served by JAX-RS or the native framework
HashMap<String, String> parts = new HashMap<>();
static final Set<String> knownPrefixes = Set.of("cluster", "node", "collections", "cores", "c");
public V2HttpCall(
SolrDispatchFilter solrDispatchFilter,
CoreContainer cc,
HttpServletRequest request,
HttpServletResponse response,
boolean retry) {
super(solrDispatchFilter, cc, request, response, retry);
}
@Override
@SuppressForbidden(
reason =
"Set the thread contextClassLoader for all 3rd party dependencies that we cannot control")
protected void init() throws Exception {
queryParams = SolrRequestParsers.parseQueryString(req.getQueryString());
String path = this.path;
final String fullPath = path = path.substring(7); // strip off '/____v2'
try {
pathSegments = PathTrie.getPathSegments(path);
if (pathSegments.size() == 0
|| (pathSegments.size() == 1 && path.endsWith(CommonParams.INTROSPECT))) {
api =
new Api(null) {
@Override
public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
rsp.add(
"documentation",
"https://solr.apache.org/guide/solr/latest/configuration-guide/v2-api.html");
rsp.add("description", "V2 API root path");
}
};
initAdminRequest(path);
return;
} else {
prefix = pathSegments.get(0);
}
boolean isCompositeApi = false;
api = getApiInfo(cores.getRequestHandlers(), path, req.getMethod(), fullPath, parts);
if (knownPrefixes.contains(prefix)) {
if (api != null) {
isCompositeApi = api instanceof CompositeApi;
if (!isCompositeApi) {
initAdminRequest(path);
return;
}
}
} else { // custom plugin
if (api != null) {
initAdminRequest(path);
return;
}
assert core == null;
}
if (pathSegments.size() > 1 && ("c".equals(prefix) || "collections".equals(prefix))) {
origCorename = pathSegments.get(1);
String collectionStr = queryParams.get(COLLECTION_PROP, origCorename);
collectionsList =
resolveCollectionListOrAlias(collectionStr); // &collection= takes precedence
if (collectionsList.isEmpty()) {
if (!path.endsWith(CommonParams.INTROSPECT)) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST, "Resolved list of collections is empty");
}
} else 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);
} else {
String collectionName = collectionsList.get(0);
// Certain HTTP methods are only used for admin APIs, check for those and short-circuit
if (List.of("delete").contains(req.getMethod().toLowerCase(Locale.ROOT))) {
initAdminRequest(path);
return;
}
boolean isPreferLeader = (path.endsWith("/update") || path.contains("/update/"));
core = getCoreByCollection(collectionName, isPreferLeader);
if (core == null) {
// this collection exists , but this node does not have a replica for that collection
extractRemotePath(collectionName, collectionName);
if (action == REMOTEQUERY) {
action = ADMIN_OR_REMOTEQUERY;
coreUrl = coreUrl.replace("/solr/", "/solr/____v2/c/");
this.path = path = path.substring(prefix.length() + collectionName.length() + 2);
return;
}
}
}
} else if ("cores".equals(prefix)) {
origCorename = pathSegments.get(1);
core = cores.getCore(origCorename);
}
// We didn't find a core, so we're either an error or a Jersey 'ADMIN' api
if (core == null) {
// initAdminRequest handles "custom plugin" Jersey APIs, as well as in-built ones
initAdminRequest(path);
return;
}
Thread.currentThread().setContextClassLoader(core.getResourceLoader().getClassLoader());
this.path = path = path.substring(prefix.length() + pathSegments.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);
}
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 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);
}
});
}
return api;
}
public List<String> getPathSegments() {
return pathSegments;
}
public static class CompositeApi extends Api {
private final List<Api> apis = new ArrayList<>();
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;
}
}
private boolean invokeJerseyRequest(
CoreContainer cores,
SolrCore core,
ApplicationHandler jerseyHandler,
PluginBag<SolrRequestHandler> requestHandlers,
SolrQueryResponse rsp) {
return invokeJerseyRequest(cores, core, jerseyHandler, requestHandlers, rsp, Map.of());
}
private boolean invokeJerseyRequest(
CoreContainer cores,
SolrCore core,
ApplicationHandler jerseyHandler,
PluginBag<SolrRequestHandler> requestHandlers,
SolrQueryResponse rsp,
Map<String, String> additionalProperties) {
final ContainerRequest containerRequest =
ContainerRequestUtils.createContainerRequest(
req, response, jerseyHandler.getConfiguration());
// Set properties that may be used by Jersey filters downstream
containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_REQUEST, solrReq);
containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_RESPONSE, rsp);
containerRequest.setProperty(RequestContextKeys.CORE_CONTAINER, cores);
containerRequest.setProperty(
RequestContextKeys.RESOURCE_TO_RH_MAPPING, requestHandlers.getJaxrsRegistry());
containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_REQ, req);
containerRequest.setProperty(RequestContextKeys.REQUEST_TYPE, requestType);
containerRequest.setProperty(RequestContextKeys.SOLR_PARAMS, queryParams);
containerRequest.setProperty(RequestContextKeys.COLLECTION_LIST, collectionsList);
containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_RSP, response);
if (core != null) {
containerRequest.setProperty(RequestContextKeys.SOLR_CORE, core);
}
if (additionalProperties != null) {
for (Map.Entry<String, String> entry : additionalProperties.entrySet()) {
containerRequest.setProperty(entry.getKey(), entry.getValue());
}
}
try {
servedByJaxRs = true;
jerseyHandler.handle(containerRequest);
return containerRequest.getProperty(RequestContextKeys.NOT_FOUND_FLAG) == null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Differentiate between "admin" and "remotequery"-type requests; executing each as appropriate.
*
* <p>The JAX-RS framework used by {@link V2HttpCall} doesn't provide any easy way to check in
* advance whether a Jersey application can handle an incoming request. This, in turn, makes it
* difficult to classify requests as being "admin" or "core, "local" or "remote". The only option
* is to submit the request to the JAX-RS application and see whether a quick "404" flag comes
* back, or not.
*
* <p>This method uses this strategy to differentiate between admin requests that don't require a
* {@link SolrCore}, but whose path happen to contain a core/collection name (e.g.
* ADDREPLICAPROP's path of
* /collections/collName/shards/shardName/replicas/replicaName/properties), and "REMOTEQUERY"
* requests which do require a local SolrCore to process.
*/
@Override
protected void handleAdminOrRemoteRequest() throws IOException {
final Map<String, String> suppressNotFoundProp =
Map.of(RequestContextKeys.SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION, "true");
SolrQueryResponse solrResp = new SolrQueryResponse();
final boolean jerseyResourceFound =
invokeJerseyRequest(
cores,
null,
cores.getJerseyApplicationHandler(),
cores.getRequestHandlers(),
solrResp,
suppressNotFoundProp);
if (jerseyResourceFound) {
logAndFlushAdminRequest(solrResp);
return;
}
// If no admin/container-level Jersey resource was found for this API, then this should be
// treated as a REMOTEQUERY
sendRemoteQuery();
}
@Override
protected void handleAdmin(SolrQueryResponse solrResp) {
if (api == null) {
invokeJerseyRequest(
cores, null, cores.getJerseyApplicationHandler(), cores.getRequestHandlers(), solrResp);
} else {
SolrCore.preDecorateResponse(solrReq, solrResp);
try {
api.call(this.solrReq, solrResp);
} catch (Exception e) {
solrResp.setException(e);
} finally {
SolrCore.postDecorateResponse(handler, solrReq, solrResp);
}
}
}
/**
* Executes the API or Jersey resource corresponding to a core-level request.
*
* <p>{@link Api}-based endpoints do this by invoking {@link Api#call(SolrQueryRequest,
* SolrQueryResponse)}.
*
* <p>JAX-RS-based endpoints must check both the core-level and container-level JAX-RS
* applications as the resource for a given "core-level request" might be registered in either
* place, depending on various legacy factors like the request handler it is associated with. In
* support of this, the JAX-RS codepath sets a flag to suppress the normal 404 error response when
* checking the first of the two JAX-RS applications.
*
* @see org.apache.solr.jersey.NotFoundExceptionMapper
*/
@Override
protected void executeCoreRequest(SolrQueryResponse rsp) {
if (api == null) {
final Map<String, String> suppressNotFoundProp =
Map.of(RequestContextKeys.SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION, "true");
final boolean resourceFound =
invokeJerseyRequest(
cores,
core,
core.getJerseyApplicationHandler(),
core.getRequestHandlers(),
rsp,
suppressNotFoundProp);
if (!resourceFound) {
// Whatever this API call is, it's not a core-level request so make sure we free our
// SolrCore counter
core.close();
core = null;
response.getHeaderNames().stream().forEach(name -> response.setHeader(name, null));
invokeJerseyRequest(
cores, null, cores.getJerseyApplicationHandler(), cores.getRequestHandlers(), rsp);
}
} else {
SolrCore.preDecorateResponse(solrReq, rsp);
try {
api.call(solrReq, rsp);
} catch (Exception e) {
rsp.setException(e);
} finally {
SolrCore.postDecorateResponse(handler, solrReq, rsp);
}
}
}
@Override
protected void populateTracingSpan(Span span) {
// Set db.instance
String coreOrColName = this.origCorename;
if (coreOrColName == null) {
Map<String, String> pathTemplateValues = getUrlParts(); // == solrReq.getPathTemplateValues()
coreOrColName = pathTemplateValues.get("collection");
if (coreOrColName == null) {
coreOrColName = pathTemplateValues.get("core");
}
}
TraceUtils.setDbInstance(span, coreOrColName);
// Get the templatize-ed path, ex: "/c/{collection}"
final String path = computeEndpointPath();
final String verb = req.getMethod().toLowerCase(Locale.ROOT);
span.updateName(verb + ":" + path);
}
/** Example: /c/collection1/ and template map collection->collection1 produces /c/{collection}. */
private String computeEndpointPath() {
// It's not ideal to compute this; let's try to transition away from hitting this code path
// by using Annotation APIs
// collection -> myColName
final Map<String, String> pathTemplateKeyVal = getUrlParts();
// myColName -> collection
final Map<String, String> pathTemplateValKey;
if (pathTemplateKeyVal.isEmpty()) { // typical
pathTemplateValKey = Collections.emptyMap();
} else if (pathTemplateKeyVal.size() == 1) { // typical
Map.Entry<String, String> entry = pathTemplateKeyVal.entrySet().iterator().next();
pathTemplateValKey = Map.of(entry.getValue(), entry.getKey());
} else { // uncommon
pathTemplateValKey = new HashMap<>();
for (Map.Entry<String, String> entry : pathTemplateKeyVal.entrySet()) {
pathTemplateValKey.put(entry.getValue(), entry.getKey());
}
}
final StringBuilder builder = new StringBuilder();
for (String segment : pathSegments) {
builder.append('/');
String key = pathTemplateValKey.get(segment);
if (key == null) {
builder.append(segment);
} else {
builder.append('{').append(key).append('}');
}
}
return builder.toString();
}
@Override
protected void writeResponse(
SolrQueryResponse solrRsp, QueryResponseWriter responseWriter, Method reqMethod)
throws IOException {
// JAX-RS has its own code that flushes out the Response to the relevant output stream, so we
// no-op here if the request was already handled via JAX-RS
if (!servedByJaxRs) {
super.writeResponse(solrRsp, responseWriter, reqMethod);
}
}
@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();
}
}