org.apache.solr.rest.RestManager Maven / Gradle / Ivy
Show all versions of solr-core Show documentation
/*
* 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.rest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Supports runtime mapping of REST API endpoints to ManagedResource implementations; endpoints can
* be registered at either the /schema or /config base paths, depending on which base path is more
* appropriate for the type of managed resource.
*/
public class RestManager {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String SCHEMA_BASE_PATH = "/schema";
public static final String MANAGED_ENDPOINT = "/managed";
// used for validating resourceIds provided during registration
private static final Pattern resourceIdRegex = Pattern.compile("(/config|/schema)(/.*)");
private static final boolean DECODE = true;
/** Used internally to keep track of registrations during core initialization */
private static class ManagedResourceRegistration {
String resourceId;
Class implClass;
Set observers = new LinkedHashSet<>();
private ManagedResourceRegistration(
String resourceId,
Class implClass,
ManagedResourceObserver observer) {
this.resourceId = resourceId;
this.implClass = implClass;
if (observer != null) {
this.observers.add(observer);
}
}
/** Returns resourceId, class, and number of observers of this registered resource */
public Map getInfo() {
Map info = new HashMap<>();
info.put("resourceId", resourceId);
info.put("class", implClass.getName());
info.put("numObservers", String.valueOf(observers.size()));
return info;
}
}
/**
* Per-core registry of ManagedResources found during core initialization.
*
* Registering of managed resources can happen before the RestManager is fully initialized. To
* avoid timing issues, resources register themselves and then the RestManager initializes all
* ManagedResources before the core is activated.
*/
public static class Registry {
private Map registered = new TreeMap<>();
// maybe null until there is a restManager
private RestManager initializedRestManager = null;
// REST API endpoints that need to be protected against dynamic endpoint creation
private final Set reservedEndpoints = new HashSet<>();
private final Pattern reservedEndpointsPattern;
public Registry() {
reservedEndpoints.add(SCHEMA_BASE_PATH + MANAGED_ENDPOINT);
reservedEndpointsPattern = getReservedEndpointsPattern();
}
/** Returns the set of non-registrable endpoints. */
public Set getReservedEndpoints() {
return Collections.unmodifiableSet(reservedEndpoints);
}
/**
* Returns a Pattern, to be used with Matcher.matches(), that will recognize prefixes or full
* matches against reserved endpoints that need to be protected against dynamic endpoint
* registration. group(1) will contain the match regardless of whether it's a full match or a
* prefix.
*/
private Pattern getReservedEndpointsPattern() {
// Match any of the reserved endpoints exactly, or followed by a slash and more stuff
StringBuilder builder = new StringBuilder();
builder.append("(");
boolean notFirst = false;
for (String reservedEndpoint : reservedEndpoints) {
if (notFirst) {
builder.append("|");
} else {
notFirst = true;
}
builder.append(reservedEndpoint);
}
builder.append(")(?:|/.*)");
return Pattern.compile(builder.toString());
}
/** Get a view of the currently registered resources. */
public Collection getRegistered() {
return Collections.unmodifiableCollection(registered.values());
}
/**
* Register the need to use a ManagedResource; this method is typically called by a Solr
* component during core initialization to register itself as an observer of a specific type of
* ManagedResource. As many Solr components may share the same ManagedResource, this method only
* serves to associate the observer with an endpoint and implementation class. The actual
* construction of the ManagedResource and loading of data from storage occurs later once the
* RestManager is fully initialized.
*
* @param resourceId - An endpoint in the Rest API to manage the resource; must start with
* /config and /schema.
* @param implClass - Class that implements ManagedResource.
* @param observer - Solr component that needs to know when the data being managed by the
* ManagedResource is loaded, such as a TokenFilter.
*/
public synchronized void registerManagedResource(
String resourceId,
Class implClass,
ManagedResourceObserver observer) {
if (resourceId == null)
throw new IllegalArgumentException(
"Must provide a non-null resourceId to register a ManagedResource!");
Matcher resourceIdValidator = resourceIdRegex.matcher(resourceId);
if (!resourceIdValidator.matches()) {
String errMsg =
String.format(
Locale.ROOT,
"Invalid resourceId '%s'; must start with %s.",
resourceId,
SCHEMA_BASE_PATH);
throw new SolrException(ErrorCode.SERVER_ERROR, errMsg);
}
// protect reserved REST API endpoints from being used by another
Matcher reservedEndpointsMatcher = reservedEndpointsPattern.matcher(resourceId);
if (reservedEndpointsMatcher.matches()) {
throw new SolrException(
ErrorCode.SERVER_ERROR,
reservedEndpointsMatcher.group(1)
+ " is a reserved endpoint used by the Solr REST API!");
}
// IMPORTANT: this code should assume there is no RestManager at this point
// it's ok to re-register the same class for an existing path
ManagedResourceRegistration reg = registered.get(resourceId);
if (reg != null) {
if (!implClass.equals(reg.implClass)) {
String errMsg =
String.format(
Locale.ROOT,
"REST API path %s already registered to instances of %s",
resourceId,
reg.implClass.getName());
throw new SolrException(ErrorCode.SERVER_ERROR, errMsg);
}
if (observer != null) {
reg.observers.add(observer);
if (log.isInfoEnabled()) {
log.info(
"Added observer of type {} to existing ManagedResource {}",
observer.getClass().getName(),
resourceId);
}
}
} else {
registered.put(
resourceId, new ManagedResourceRegistration(resourceId, implClass, observer));
if (log.isInfoEnabled()) {
log.info(
"Registered ManagedResource impl {} for path {}", implClass.getName(), resourceId);
}
}
// there may be a RestManager, in which case, we want to add this new ManagedResource
// immediately
if (initializedRestManager != null
&& initializedRestManager.getManagedResourceOrNull(resourceId) == null) {
initializedRestManager.addRegisteredResource(registered.get(resourceId));
}
}
}
/**
* Request handling needs a lightweight object to delegate a request to. ManagedResource
* implementations are heavy-weight objects that live for the duration of a SolrCore, so this
* class acts as the proxy between the request handler and a ManagedResource when doing request
* processing.
*/
public static class ManagedEndpoint extends BaseSolrResource {
final RestManager restManager;
public ManagedEndpoint(RestManager restManager) {
this.restManager = restManager;
}
/** Determines the ManagedResource resourceId from the request path. */
public static String resolveResourceId(final String path) {
String resourceId;
resourceId = URLDecoder.decode(path, StandardCharsets.UTF_8);
int at = resourceId.indexOf("/schema");
if (at == -1) {
at = resourceId.indexOf("/config");
}
if (at > 0) {
resourceId = resourceId.substring(at);
}
// all resources are registered with the leading slash
if (!resourceId.startsWith("/")) resourceId = "/" + resourceId;
return resourceId;
}
protected ManagedResource managedResource;
protected String childId;
/**
* Initialize objects needed to handle a request to the REST API. Specifically, we lookup the
* RestManager using the ThreadLocal SolrRequestInfo and then dynamically locate the
* ManagedResource associated with the request URI.
*/
@Override
public void doInit(SolrQueryRequest solrRequest, SolrQueryResponse solrResponse) {
super.doInit(solrRequest, solrResponse);
final String resourceId = resolveResourceId(solrRequest.getPath());
managedResource = restManager.getManagedResourceOrNull(resourceId);
if (managedResource == null) {
int lastSlashAt;
String parentResourceId;
String initialResourceId = resourceId;
// Check if we have a registered endpoint, going one level up each time...
do {
lastSlashAt = initialResourceId.lastIndexOf('/');
parentResourceId = resourceId.substring(0, lastSlashAt);
log.debug(
"Resource not found for {}, looking for parent: {}", resourceId, parentResourceId);
managedResource = restManager.getManagedResourceOrNull(parentResourceId);
initialResourceId = parentResourceId;
} while (managedResource == null && initialResourceId.lastIndexOf("/") != -1);
if (managedResource != null) {
// verify this resource supports child resources
if (!(managedResource instanceof ManagedResource.ChildResourceSupport)) {
String errMsg =
String.format(
Locale.ROOT,
"%s does not support child resources!",
managedResource.getResourceId());
throw new SolrException(ErrorCode.BAD_REQUEST, errMsg);
}
childId = resourceId.substring(lastSlashAt + 1);
log.debug("Found parent resource {} for child: {}", parentResourceId, childId);
}
}
if (managedResource == null) {
final String method = getSolrRequest().getHttpMethod();
if ("PUT".equals(method) || "POST".equals(method)) {
// delegate create requests to the RestManager
managedResource = restManager.endpoint;
} else {
throw new SolrException(
ErrorCode.BAD_REQUEST, "No REST managed resource registered for path " + resourceId);
}
}
log.info("Found ManagedResource [{}] for {}", managedResource, resourceId);
}
public void delegateRequestToManagedResource() {
SolrQueryRequest req = getSolrRequest();
final String method = req.getHttpMethod();
try {
switch (method) {
case "HEAD":
managedResource.doGet(this, childId);
doHead(this);
break;
case "GET":
managedResource.doGet(this, childId);
break;
case "PUT":
managedResource.doPut(this, parseJsonFromRequestBody(req));
break;
case "POST":
managedResource.doPost(this, parseJsonFromRequestBody(req));
break;
case "DELETE":
doDelete();
break;
}
} catch (Exception e) {
getSolrResponse().setException(e);
}
handlePostExecution(log);
}
private void doHead(ManagedEndpoint managedEndpoint) {
// Setting the response to blank clears the content out.
NamedList