blob: ea1d3c08072861fdb34340ee39db5cff0d54e5ec [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.rest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.net.URLDecoder;
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.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;
import static org.apache.solr.common.util.Utils.fromJSON;
/**
* 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<? extends ManagedResource> implClass;
Set<ManagedResourceObserver> observers = new LinkedHashSet<>();
private ManagedResourceRegistration(String resourceId,
Class<? extends ManagedResource> 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<String,String> getInfo() {
Map<String,String> 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<String,ManagedResourceRegistration> 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<String> 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<String> 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<ManagedResourceRegistration> 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<? extends ManagedResource> 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;
try {
resourceId = URLDecoder.decode(path, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // shouldn't happen
}
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) {
// see if we have a registered endpoint one-level up ...
int lastSlashAt = resourceId.lastIndexOf('/');
if (lastSlashAt != -1) {
String parentResourceId = resourceId.substring(0,lastSlashAt);
log.info("Resource not found for {}, looking for parent: {}",
resourceId, parentResourceId);
managedResource = restManager.getManagedResourceOrNull(parentResourceId);
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.info("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 "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);
}
protected void doDelete() {
// only delegate delete child resources to the ManagedResource
// as deleting the actual resource is best handled by the
// RestManager
if (childId != null) {
try {
managedResource.doDeleteChild(this, childId);
} catch (Exception e) {
getSolrResponse().setException(e);
}
} else {
try {
restManager.deleteManagedResource(managedResource);
} catch (Exception e) {
getSolrResponse().setException(e);
}
}
handlePostExecution(log);
}
protected Object parseJsonFromRequestBody(SolrQueryRequest req) {
Iterator<ContentStream> iter = req.getContentStreams().iterator();
if (iter.hasNext()) {
try (Reader reader = iter.next().getReader()) {
return fromJSON(reader);
} catch (IOException ioExc) {
throw new SolrException(ErrorCode.SERVER_ERROR, ioExc);
}
}
throw new SolrException(ErrorCode.BAD_REQUEST, "No JSON body found in request!");
}
@Override
protected void addDeprecatedWarning() {
//this is not deprecated
}
} // end ManagedEndpoint class
/**
* The RestManager itself supports some endpoints for creating and listing managed resources.
* Effectively, this resource provides the API endpoint for doing CRUD on the registry.
*/
private static class RestManagerManagedResource extends ManagedResource {
private static final String REST_MANAGER_STORAGE_ID = "/rest/managed";
private final RestManager restManager;
public RestManagerManagedResource(RestManager restManager) throws SolrException {
super(REST_MANAGER_STORAGE_ID, restManager.loader, restManager.storageIO);
this.restManager = restManager;
}
/**
* Overrides the parent impl to handle FileNotFoundException better
*/
@Override
protected synchronized void reloadFromStorage() throws SolrException {
String resourceId = getResourceId();
Object data = null;
try {
data = storage.load(resourceId);
} catch (FileNotFoundException fnf) {
// this is ok - simply means there are no managed components added yet
} catch (IOException ioExc) {
throw new SolrException(ErrorCode.SERVER_ERROR,
"Failed to load stored data for "+resourceId+" due to: "+ioExc, ioExc);
}
Object managedData = processStoredData(data);
if (managedInitArgs == null)
managedInitArgs = new NamedList<>();
if (managedData != null)
onManagedDataLoadedFromStorage(managedInitArgs, managedData);
}
/**
* Loads and initializes any ManagedResources that have been created but
* are not associated with any Solr components.
*/
@SuppressWarnings("unchecked")
@Override
protected void onManagedDataLoadedFromStorage(NamedList<?> managedInitArgs, Object managedData)
throws SolrException {
if (managedData == null) {
// this is ok - just means no managed components have been added yet
return;
}
List<Object> managedList = (List<Object>)managedData;
for (Object next : managedList) {
Map<String,String> info = (Map<String,String>)next;
String implClass = info.get("class");
String resourceId = info.get("resourceId");
Class<? extends ManagedResource> clazz = solrResourceLoader.findClass(implClass, ManagedResource.class);
ManagedResourceRegistration existingReg = restManager.registry.registered.get(resourceId);
if (existingReg == null) {
restManager.registry.registerManagedResource(resourceId, clazz, null);
} // else already registered, no need to take any action
}
}
/**
* Creates a new ManagedResource in the RestManager.
*/
@SuppressWarnings("unchecked")
@Override
public synchronized void doPut(BaseSolrResource endpoint, Object json) {
if (json instanceof Map) {
String resourceId = ManagedEndpoint.resolveResourceId(endpoint.getSolrRequest().getPath());
Map<String,String> info = (Map<String,String>)json;
info.put("resourceId", resourceId);
storeManagedData(applyUpdatesToManagedData(json));
} else {
throw new SolrException(ErrorCode.BAD_REQUEST,
"Expected Map to create a new ManagedResource but received a "+json.getClass().getName());
}
// PUT just returns success status code with an empty body
}
/**
* Registers a new {@link ManagedResource}.
*
* Called during PUT/POST processing to apply updates to the managed data passed from the client.
*/
@SuppressWarnings("unchecked")
@Override
protected Object applyUpdatesToManagedData(Object updates) {
Map<String,String> info = (Map<String,String>)updates;
// this is where we'd register a new ManagedResource
String implClass = info.get("class");
String resourceId = info.get("resourceId");
log.info("Creating a new ManagedResource of type {} at path {}",
implClass, resourceId);
Class<? extends ManagedResource> clazz =
solrResourceLoader.findClass(implClass, ManagedResource.class);
// add this new resource to the RestManager
restManager.addManagedResource(resourceId, clazz);
// we only store ManagedResources that don't have observers as those that do
// are already implicitly defined
List<Map<String,String>> managedList = new ArrayList<>();
for (ManagedResourceRegistration reg : restManager.registry.getRegistered()) {
if (reg.observers.isEmpty()) {
managedList.add(reg.getInfo());
}
}
return managedList;
}
/**
* Deleting of child resources not supported by this implementation.
*/
@Override
public void doDeleteChild(BaseSolrResource endpoint, String childId) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Delete child resource not supported!");
}
@Override
public void doGet(BaseSolrResource endpoint, String childId) {
// filter results by /schema or /config
String path = ManagedEndpoint.resolveResourceId(endpoint.getSolrRequest().getPath());
Matcher resourceIdMatcher = resourceIdRegex.matcher(path);
if (!resourceIdMatcher.matches()) {
// extremely unlikely but didn't want to squelch it either
throw new SolrException(ErrorCode.BAD_REQUEST, "Requests to path "+path+" not supported!");
}
String filter = resourceIdMatcher.group(1);
List<Map<String,String>> regList = new ArrayList<>();
for (ManagedResourceRegistration reg : restManager.registry.getRegistered()) {
if (!reg.resourceId.startsWith(filter))
continue; // doesn't match filter
if (RestManagerManagedResource.class.isAssignableFrom(reg.implClass))
continue; // internal, no need to expose to outside
regList.add(reg.getInfo());
}
endpoint.getSolrResponse().add("managedResources", regList);
}
} // end RestManagerManagedResource
protected StorageIO storageIO;
protected Registry registry;
protected Map<String,ManagedResource> managed = new TreeMap<>();
protected RestManagerManagedResource endpoint;
protected SolrResourceLoader loader;
/**
* Initializes the RestManager with the storageIO being optionally created outside of this implementation
* such as to use ZooKeeper instead of the local FS.
*/
public void init(SolrResourceLoader loader,
NamedList<String> initArgs,
StorageIO storageIO)
throws SolrException
{
log.debug("Initializing RestManager with initArgs: {}", initArgs);
if (storageIO == null)
throw new IllegalArgumentException(
"Must provide a valid StorageIO implementation to the RestManager!");
this.storageIO = storageIO;
this.loader = loader;
registry = loader.getManagedResourceRegistry();
// the RestManager provides metadata about managed resources via the /managed endpoint
// and allows you to create new ManagedResources dynamically by PUT'ing to this endpoint
endpoint = new RestManagerManagedResource(this);
endpoint.loadManagedDataAndNotify(null); // no observers for my endpoint
// responds to requests to /config/managed and /schema/managed
managed.put(SCHEMA_BASE_PATH+MANAGED_ENDPOINT, endpoint);
// init registered managed resources
if (log.isDebugEnabled()) {
log.debug("Initializing {} registered ManagedResources", registry.registered.size());
}
for (ManagedResourceRegistration reg : registry.registered.values()) {
// keep track of this for lookups during request processing
managed.put(reg.resourceId, createManagedResource(reg));
}
// this is for any new registrations that don't come through the API
// such as from adding a new fieldType to a managed schema that uses a ManagedResource
registry.initializedRestManager = this;
}
/**
* If not already registered, registers the given {@link ManagedResource} subclass
* at the given resourceId, creates an instance. Returns the corresponding instance.
*/
public synchronized ManagedResource addManagedResource(String resourceId, Class<? extends ManagedResource> clazz) {
final ManagedResource res;
final ManagedResourceRegistration existingReg = registry.registered.get(resourceId);
if (existingReg == null) {
registry.registerManagedResource(resourceId, clazz, null);
res = addRegisteredResource(registry.registered.get(resourceId));
} else {
res = getManagedResource(resourceId);
}
return res;
}
// cache a mapping of path to ManagedResource
private synchronized ManagedResource addRegisteredResource(ManagedResourceRegistration reg) {
String resourceId = reg.resourceId;
ManagedResource res = createManagedResource(reg);
managed.put(resourceId, res);
log.info("Registered new managed resource {}", resourceId);
return res;
}
/**
* Creates a ManagedResource using registration information.
*/
protected ManagedResource createManagedResource(ManagedResourceRegistration reg) throws SolrException {
ManagedResource res = null;
try {
Constructor<? extends ManagedResource> ctor =
reg.implClass.getConstructor(String.class, SolrResourceLoader.class, StorageIO.class);
res = ctor.newInstance(reg.resourceId, loader, storageIO);
res.loadManagedDataAndNotify(reg.observers);
} catch (Exception e) {
String errMsg =
String.format(Locale.ROOT,
"Failed to create new ManagedResource %s of type %s due to: %s",
reg.resourceId, reg.implClass.getName(), e);
throw new SolrException(ErrorCode.SERVER_ERROR, errMsg, e);
}
return res;
}
/**
* Returns the {@link ManagedResource} subclass instance corresponding
* to the given resourceId from the registry.
*
* @throws SolrException if no managed resource is registered with
* the given resourceId.
*/
public ManagedResource getManagedResource(String resourceId) {
ManagedResource res = getManagedResourceOrNull(resourceId);
if (res == null) {
throw new SolrException(ErrorCode.NOT_FOUND, "No ManagedResource registered for path: "+resourceId);
}
return res;
}
/**
* Returns the {@link ManagedResource} subclass instance corresponding
* to the given resourceId from the registry, or null if no resource
* has been registered with the given resourceId.
*/
public synchronized ManagedResource getManagedResourceOrNull(String resourceId) {
return managed.get(resourceId);
}
/**
* Deletes a managed resource if it is not being used by any Solr components.
*/
public synchronized void deleteManagedResource(ManagedResource res) {
String resourceId = res.getResourceId();
ManagedResourceRegistration existingReg = registry.registered.get(resourceId);
int numObservers = existingReg.observers.size();
if (numObservers > 0) {
String errMsg =
String.format(Locale.ROOT,
"Cannot delete managed resource %s as it is being used by %d Solr components",
resourceId, numObservers);
throw new SolrException(ErrorCode.FORBIDDEN, errMsg);
}
registry.registered.remove(resourceId);
managed.remove(resourceId);
try {
res.onResourceDeleted();
} catch (IOException e) {
// the resource is already deleted so just log this
log.error("Error when trying to clean-up after deleting {}",resourceId, e);
}
}
}