org.apache.solr.api.ApiBag 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.api;
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;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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 org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SpecProvider;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.JsonSchemaValidator;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.PathTrie;
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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ApiBag {
private final boolean isCoreSpecific;
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final Map> apis = new ConcurrentHashMap<>();
public ApiBag(boolean isCoreSpecific) {
this.isCoreSpecific = isCoreSpecific;
}
/**
* Register a POJO annotated with {@link EndPoint}
*
* @param o the instance to be used for invocations
*/
public synchronized List registerObject(Object o) {
List l = AnnotatedApi.getApis(o);
for (Api api : l) {
register(api, Collections.emptyMap());
}
return l;
}
public synchronized void register(Api api) {
register(api, Collections.emptyMap());
}
public synchronized void register(Api api, Map nameSubstitutes) {
try {
validateAndRegister(api, nameSubstitutes);
} catch (Exception e) {
log.error(
"Unable to register plugin: {} with spec {} :",
api.getClass().getName(),
Utils.toJSONString(api.getSpec()),
e);
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
}
}
}
/**
* PathTrie extension that combines the commands in the API being registered with any that have
* already been registered.
*
* This is only possible currently for AnnotatedApis. All other Api implementations will resort
* to the default "overwriting" behavior of PathTrie
*/
static class CommandAggregatingPathTrie extends PathTrie {
public CommandAggregatingPathTrie(Set reserved) {
super(reserved);
}
@Override
protected void attachValueToNode(PathTrie.Node node, Api o) {
if (node.getObject() == null) {
super.attachValueToNode(node, o);
return;
}
// If 'o' and 'node.obj' aren't both AnnotatedApi's then we can't aggregate the commands, so
// fallback to the default behavior
if ((!(o instanceof AnnotatedApi)) || (!(node.getObject() instanceof AnnotatedApi))) {
super.attachValueToNode(node, o);
return;
}
final AnnotatedApi beingRegistered = (AnnotatedApi) o;
final AnnotatedApi alreadyRegistered = (AnnotatedApi) node.getObject();
if (alreadyRegistered instanceof CommandAggregatingAnnotatedApi) {
final CommandAggregatingAnnotatedApi alreadyRegisteredAsCollapsing =
(CommandAggregatingAnnotatedApi) alreadyRegistered;
alreadyRegisteredAsCollapsing.combineWith(beingRegistered);
} else {
final CommandAggregatingAnnotatedApi wrapperApi =
new CommandAggregatingAnnotatedApi(alreadyRegistered);
wrapperApi.combineWith(beingRegistered);
node.setObject(wrapperApi);
}
}
}
static class CommandAggregatingAnnotatedApi extends AnnotatedApi {
private Collection combinedApis;
protected CommandAggregatingAnnotatedApi(AnnotatedApi api) {
super(api.spec, api.getEndPoint(), new HashMap<>(api.getCommands()), null);
combinedApis = new ArrayList<>();
}
public void combineWith(AnnotatedApi api) {
// Merge in new 'command' entries
boolean newCommandsAdded = false;
for (Map.Entry entry : api.getCommands().entrySet()) {
// Skip registering command if it's identical to an already registered command.
if (getCommands().containsKey(entry.getKey())
&& getCommands().get(entry.getKey()).equals(entry.getValue())) {
continue;
}
newCommandsAdded = true;
getCommands().put(entry.getKey(), entry.getValue());
}
// Reference to Api must be saved to to merge uncached values (i.e. 'spec') lazily
if (newCommandsAdded) {
combinedApis.add(api);
}
}
@Override
public ValidatingJsonMap getSpec() {
final ValidatingJsonMap aggregatedSpec = spec.getSpec();
for (AnnotatedApi combinedApi : combinedApis) {
final ValidatingJsonMap specToCombine = combinedApi.getSpec();
aggregatedSpec.getMap("commands").putAll(specToCombine.getMap("commands"));
}
return aggregatedSpec;
}
}
@SuppressWarnings({"unchecked"})
private void validateAndRegister(Api api, Map nameSubstitutes) {
ValidatingJsonMap spec = api.getSpec();
Api introspect = new IntrospectApi(api, isCoreSpecific);
List methods = spec.getList("methods", ENUM_OF, SUPPORTED_METHODS);
for (String method : methods) {
PathTrie registry = apis.get(method);
if (registry == null)
apis.put(method, registry = new CommandAggregatingPathTrie(Set.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 paths = url.getList("paths", NOT_NULL);
ValidatingJsonMap parts = url.getMap("parts", null);
if (parts != null) {
Set 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, Set.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 nameSubstitutes, PathTrie registry, String path, Api introspect) {
List 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 l, PathTrie registry, Map substitutes, Api introspect) {
ArrayList copy = new ArrayList<>(l);
copy.add("_introspect");
registry.insert(copy, substitutes, introspect);
}
public synchronized Api unregister(SolrRequest.METHOD method, String path) {
List l = PathTrie.getPathSegments(path);
List introspectPath = new ArrayList<>(l);
introspectPath.add("_introspect");
getRegistry(method.toString()).remove(introspectPath);
return getRegistry(method.toString()).remove(l);
}
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;
}
@Override
@SuppressWarnings({"unchecked"})
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);
if (m == null) {
specCopy.put("commands", Collections.singletonMap(cmd, "Command not found!"));
} else {
specCopy.put("commands", Collections.singletonMap(cmd, m));
}
}
result = specCopy;
}
if (isCoreSpecific) {
List pieces =
req.getHttpSolrCall() == null
? null
: ((V2HttpCall) req.getHttpSolrCall()).getPathSegments();
if (pieces != null) {
String prefix = "/" + pieces.get(0) + "/" + pieces.get(1);
List 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