blob: b221fba4cd3940e9451eaa71548685ca34bdac57 [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 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.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
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;
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;
public class ApiBag {
private final boolean isCoreSpecific;
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final Map<String, PathTrie<Api>> 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
*/
@SuppressWarnings({"unchecked"})
public synchronized List<Api> registerObject(Object o) {
List<Api> l = AnnotatedApi.getApis(o);
for (Api api : l) {
register(api, Collections.EMPTY_MAP);
}
return l;
}
@SuppressWarnings({"unchecked"})
public synchronized void register(Api api) {
register(api, Collections.EMPTY_MAP);
}
public synchronized void register(Api api, Map<String, String> 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);
}
}
}
@SuppressWarnings({"unchecked"})
private void validateAndRegister(Api api, Map<String, String> nameSubstitutes) {
ValidatingJsonMap spec = api.getSpec();
Api introspect = new IntrospectApi(api, isCoreSpecific);
List<String> methods = spec.getList("methods", ENUM_OF, SUPPORTED_METHODS);
for (String method : methods) {
PathTrie<Api> registry = apis.get(method);
if (registry == null) apis.put(method, registry = new PathTrie<>(ImmutableSet.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<String> paths = url.getList("paths", NOT_NULL);
ValidatingJsonMap parts = url.getMap("parts", null);
if (parts != null) {
Set<String> 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, ImmutableSet.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<String, String> nameSubstitutes, PathTrie<Api> registry, String path, Api introspect) {
List<String> 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<String> l, PathTrie<Api> registry, Map<String, String> substitutes, Api introspect) {
ArrayList<String> copy = new ArrayList<>(l);
copy.add("_introspect");
registry.insert(copy, substitutes, introspect);
}
public synchronized Api unregister(SolrRequest.METHOD method, String path) {
List<String> l = PathTrie.getPathSegments(path);
List<String> 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;
}
@SuppressWarnings({"unchecked", "rawtypes"})
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<String> pieces = req.getHttpSolrCall() == null ? null : ((V2HttpCall) req.getHttpSolrCall()).pieces;
if (pieces != null) {
String prefix = "/" + pieces.get(0) + "/" + pieces.get(1);
List<String> 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 l = (List) rsp.getValues().get("spec");
if (l == null) rsp.getValues().add("spec", l = new ArrayList());
l.add(result);
}
}
public static Map<String, JsonSchemaValidator> getParsedSchema(ValidatingJsonMap commands) {
Map<String, JsonSchemaValidator> validators = new HashMap<>();
for (Object o : commands.entrySet()) {
@SuppressWarnings({"rawtypes"})
Map.Entry cmd = (Map.Entry) o;
try {
validators.put((String) cmd.getKey(), new JsonSchemaValidator((Map) cmd.getValue()));
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in api spec", e);
}
}
return validators;
}
private void verifyCommands(ValidatingJsonMap spec) {
ValidatingJsonMap commands = spec.getMap("commands", null);
if (commands == null) return;
getParsedSchema(commands);
}
private Set<String> getWildCardNames(List<String> paths) {
Set<String> wildCardNames = new HashSet<>();
for (String path : paths) {
List<String> p = PathTrie.getPathSegments(path);
for (String s : p) {
String wildCard = PathTrie.templateName(s);
if (wildCard != null) wildCardNames.add(wildCard);
}
}
return wildCardNames;
}
public Api lookup(String path, String httpMethod, Map<String, String> parts) {
if (httpMethod == null) {
for (PathTrie<Api> trie : apis.values()) {
Api api = trie.lookup(path, parts);
if (api != null) return api;
}
return null;
} else {
PathTrie<Api> registry = apis.get(httpMethod);
if (registry == null) return null;
return registry.lookup(path, parts);
}
}
public static class ReqHandlerToApi extends Api implements PermissionNameProvider {
SolrRequestHandler rh;
public ReqHandlerToApi(SolrRequestHandler rh, SpecProvider spec) {
super(spec);
this.rh = rh;
}
@Override
public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
rh.handleRequest(req, rsp);
}
@Override
public Name getPermissionName(AuthorizationContext ctx) {
if (rh instanceof PermissionNameProvider) {
return ((PermissionNameProvider) rh).getPermissionName(ctx);
}
return null;
}
}
public static List<Api> wrapRequestHandlers(final SolrRequestHandler rh, String... specs) {
ImmutableList.Builder<Api> b = ImmutableList.builder();
for (String spec : specs) b.add(new ReqHandlerToApi(rh, Utils.getSpec(spec)));
return b.build();
}
public static final SpecProvider EMPTY_SPEC = () -> ValidatingJsonMap.EMPTY;
public static final String HANDLER_NAME = "handlerName";
public static final Set<String> KNOWN_TYPES = ImmutableSet.of("string", "boolean", "list", "int", "double", "object");
public PathTrie<Api> getRegistry(String method) {
return apis.get(method);
}
public void registerLazy(PluginBag.PluginHolder<SolrRequestHandler> holder, PluginInfo info) {
String specName = info.attributes.get("spec");
if (specName == null) specName = "emptySpec";
register(new LazyLoadedApi(Utils.getSpec(specName), holder), Collections.singletonMap(HANDLER_NAME, info.attributes.get(NAME)));
}
public static SpecProvider constructSpec(PluginInfo info) {
Object specObj = info == null ? null : info.attributes.get("spec");
if (specObj == null) specObj = "emptySpec";
if (specObj instanceof Map) {
@SuppressWarnings({"rawtypes"})
Map map = (Map) specObj;
return () -> ValidatingJsonMap.getDeepCopy(map, 4, false);
} else {
return Utils.getSpec((String) specObj);
}
}
@SuppressWarnings({"rawtypes"})
public static List<CommandOperation> getCommandOperations(ContentStream stream, Map<String, JsonSchemaValidator> validators, boolean validate) {
List<CommandOperation> parsedCommands = null;
try {
parsedCommands = CommandOperation.readCommands(Collections.singleton(stream), new NamedList());
} catch (IOException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unable to parse commands",e);
}
if (validators == null || !validate) { // no validation possible because we do not have a spec
return parsedCommands;
}
List<CommandOperation> commandsCopy = CommandOperation.clone(parsedCommands);
for (CommandOperation cmd : commandsCopy) {
JsonSchemaValidator validator = validators.get(cmd.name);
if (validator == null) {
cmd.addError(formatString("Unknown operation ''{0}'' available ops are ''{1}''", cmd.name,
validators.keySet()));
continue;
} else {
List<String> errs = validator.validateJson(cmd.getCommandData());
if (errs != null){
// otherwise swallowed in solrj tests, and just get "Error in command payload" in test log
// which is quite unhelpful.
log.error("Command errors for {}:{}", cmd.name, errs );
for (String err : errs) cmd.addError(err);
}
}
}
@SuppressWarnings({"rawtypes"})
List<Map> errs = CommandOperation.captureErrors(commandsCopy);
if (!errs.isEmpty()) {
throw new ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "Error in command payload", errs);
}
return commandsCopy;
}
public static class ExceptionWithErrObject extends SolrException {
@SuppressWarnings({"rawtypes"})
private List<Map> errs;
public ExceptionWithErrObject(ErrorCode code, String msg, @SuppressWarnings({"rawtypes"})List<Map> errs) {
super(code, msg);
this.errs = errs;
}
@SuppressWarnings({"rawtypes"})
public List<Map> getErrs() {
return errs;
}
@Override
public String getMessage() {
return super.getMessage() + ", errors: " + getErrs() + ", ";
}
}
public static class LazyLoadedApi extends Api {
private final PluginBag.PluginHolder<SolrRequestHandler> holder;
private Api delegate;
protected LazyLoadedApi(SpecProvider specProvider, PluginBag.PluginHolder<SolrRequestHandler> lazyPluginHolder) {
super(specProvider);
this.holder = lazyPluginHolder;
}
@Override
public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
if (!holder.isLoaded()) {
delegate = new ReqHandlerToApi(holder.get(), ApiBag.EMPTY_SPEC);
}
delegate.call(req, rsp);
}
}
}