All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.api.ApiBag Maven / Gradle / Ivy

There is a newer version: 9.6.1
Show newest version
/*
 * 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.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> apis = new ConcurrentHashMap<>();

  public ApiBag(boolean isCoreSpecific) {
    this.isCoreSpecific = isCoreSpecific;
  }

  public synchronized void register(Api api, Map nameSubstitutes) {
    try {
      validateAndRegister(api, nameSubstitutes);
    } catch (Exception e) {
      log.error("Unable to register plugin:" + api.getClass().getName() + "with spec :" + Utils.toJSONString(api.getSpec()), e);
      if (e instanceof RuntimeException) {
        throw (RuntimeException) e;
      } else {
        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
      }

    }
  }

  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 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 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, 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 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 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;
    }

    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()).pieces;
        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 l = (List) rsp.getValues().get("spec");
      if (l == null) rsp.getValues().add("spec", l = new ArrayList());
      l.add(result);
    }
  }

  public static Map getParsedSchema(ValidatingJsonMap commands) {
    Map validators = new HashMap<>();
    for (Object o : commands.entrySet()) {
      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 getWildCardNames(List paths) {
    Set wildCardNames = new HashSet<>();
    for (String path : paths) {
      List 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 parts) {
    if (httpMethod == null) {
      for (PathTrie trie : apis.values()) {
        Api api = trie.lookup(path, parts);
        if (api != null) return api;
      }
      return null;
    } else {
      PathTrie 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 wrapRequestHandlers(final SolrRequestHandler rh, String... specs) {
    ImmutableList.Builder 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 KNOWN_TYPES = ImmutableSet.of("string", "boolean", "list", "int", "double", "object");

  public PathTrie getRegistry(String method) {
    return apis.get(method);
  }

  public void registerLazy(PluginBag.PluginHolder 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) {
      Map map = (Map) specObj;
      return () -> ValidatingJsonMap.getDeepCopy(map, 4, false);
    } else {
      return Utils.getSpec((String) specObj);
    }
  }

  public static List getCommandOperations(ContentStream stream, Map validators, boolean validate) {
    List 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 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 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);
        }
      }

    }
    List 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 {
    private List errs;

    public ExceptionWithErrObject(ErrorCode code, String msg, List errs) {
      super(code, msg);
      this.errs = errs;
    }

    public List getErrs() {
      return errs;
    }

    public String toString() {
      return super.toString() + ", errors: " + getErrs() + ", ";
    }
  }

  public static class LazyLoadedApi extends Api {

    private final PluginBag.PluginHolder holder;
    private Api delegate;

    protected LazyLoadedApi(SpecProvider specProvider, PluginBag.PluginHolder 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);
    }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy