org.apache.solr.api.ContainerPluginsRegistry 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.lucene.util.IOUtils.closeWhileHandlingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import java.io.Closeable;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Phaser;
import org.apache.lucene.util.ResourceLoaderAware;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.request.beans.PluginMeta;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.cloud.ClusterPropertiesListener;
import org.apache.solr.common.util.CollectionUtil;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.PathTrie;
import org.apache.solr.common.util.ReflectMapWriter;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.PluginInfo;
import org.apache.solr.pkg.SolrPackageLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.SolrJacksonAnnotationInspector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class manages the container-level plugins and their Api-s. It is responsible for adding /
* removing / replacing the plugins according to the updated configuration obtained from {@link
* ClusterPluginsSource#plugins()}.
*
* Plugins instantiated by this class may implement zero or more {@link Api}-s, which are then
* registered in the CoreContainer {@link ApiBag}. They may be also post-processed for additional
* functionality by {@link PluginRegistryListener}-s registered with this class.
*/
public class ContainerPluginsRegistry implements ClusterPropertiesListener, MapWriter, Closeable {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String CLUSTER_PLUGIN_EDIT_ENABLED = "solr.cluster.plugin.edit.enabled";
private static final ObjectMapper mapper =
SolrJacksonAnnotationInspector.createObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.disable(MapperFeature.AUTO_DETECT_FIELDS);
private final List listeners = new CopyOnWriteArrayList<>();
private final CoreContainer coreContainer;
private final ApiBag containerApiBag;
private final ClusterPluginsSource pluginsSource;
private final Map currentPlugins = new HashMap<>();
private Phaser phaser;
@Override
public boolean onChange(Map properties) {
refresh();
Phaser localPhaser = phaser; // volatile read
if (localPhaser != null) {
localPhaser.arrive();
}
return false;
}
/**
* A phaser that will advance phases every time {@link #onChange(Map)} is called. Useful for
* allowing tests to know when a new configuration is finished getting set.
*/
@VisibleForTesting
public void setPhaser(Phaser phaser) {
phaser.register();
this.phaser = phaser;
}
public void registerListener(PluginRegistryListener listener) {
listeners.add(listener);
}
public void unregisterListener(PluginRegistryListener listener) {
listeners.remove(listener);
}
public ContainerPluginsRegistry(
CoreContainer coreContainer, ApiBag apiBag, ClusterPluginsSource pluginsSource) {
this.coreContainer = coreContainer;
this.containerApiBag = apiBag;
this.pluginsSource = pluginsSource;
}
@Override
public synchronized void writeMap(EntryWriter ew) throws IOException {
currentPlugins.forEach(ew.getBiConsumer());
}
@Override
public synchronized void close() throws IOException {
currentPlugins
.values()
.forEach(
apiInfo -> {
if (apiInfo.instance instanceof Closeable) {
IOUtils.closeQuietly((Closeable) apiInfo.instance);
}
});
}
public synchronized ApiInfo getPlugin(String name) {
return currentPlugins.get(name);
}
static class PluginMetaHolder {
private final Map original;
private final PluginMeta meta;
PluginMetaHolder(Map original) throws IOException {
this.original = original;
meta = mapper.readValue(Utils.toJSON(original), PluginMeta.class);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PluginMetaHolder) {
PluginMetaHolder that = (PluginMetaHolder) obj;
return Objects.equals(this.original, that.original);
}
return false;
}
@Override
public int hashCode() {
return original.hashCode();
}
}
@SuppressWarnings("unchecked")
public synchronized void refresh() {
Map pluginInfos;
try {
pluginInfos = pluginsSource.plugins();
} catch (IOException e) {
log.error("Could not read plugins data", e);
return;
}
Map newState = CollectionUtil.newHashMap(pluginInfos.size());
for (Map.Entry e : pluginInfos.entrySet()) {
try {
newState.put(e.getKey(), new PluginMetaHolder((Map) e.getValue()));
} catch (Exception exp) {
log.error("Invalid apiInfo configuration :", exp);
}
}
Map currentState = new HashMap<>();
for (Map.Entry e : currentPlugins.entrySet()) {
currentState.put(e.getKey(), e.getValue().holder);
}
Map diff = compareMaps(currentState, newState);
if (diff == null) return; // nothing has changed
for (Map.Entry e : diff.entrySet()) {
if (e.getValue() == Diff.UNCHANGED) continue;
if (e.getValue() == Diff.REMOVED) {
ApiInfo apiInfo = currentPlugins.remove(e.getKey());
if (apiInfo == null) continue;
listeners.forEach(listener -> listener.deleted(apiInfo));
for (ApiHolder holder : apiInfo.holders) {
Api old =
containerApiBag.unregister(
holder.api.getEndPoint().method()[0],
getActualPath(apiInfo, holder.api.getEndPoint().path()[0]));
if (old instanceof Closeable) {
closeWhileHandlingException((Closeable) old);
}
}
} else {
// ADDED or UPDATED
PluginMetaHolder info = newState.get(e.getKey());
List errs = new ArrayList<>();
ApiInfo apiInfo = new ApiInfo(info, errs);
if (!errs.isEmpty()) {
log.error(StrUtils.join(errs, ','));
continue;
}
try {
apiInfo.init();
} catch (Exception exp) {
log.error("Cannot install apiInfo ", exp);
continue;
}
if (e.getValue() == Diff.ADDED) {
// this plugin is totally new
for (ApiHolder holder : apiInfo.holders) {
containerApiBag.register(holder, getTemplateVars(apiInfo.info));
}
currentPlugins.put(e.getKey(), apiInfo);
final ApiInfo apiInfoFinal = apiInfo;
listeners.forEach(listener -> listener.added(apiInfoFinal));
} else {
// this plugin is being updated
ApiInfo old = currentPlugins.put(e.getKey(), apiInfo);
for (ApiHolder holder : apiInfo.holders) {
// register all new paths
containerApiBag.register(holder, getTemplateVars(apiInfo.info));
}
final ApiInfo apiInfoFinal = apiInfo;
listeners.forEach(listener -> listener.modified(old, apiInfoFinal));
if (old != null) {
// this is an update of the plugin. But, it is possible that
// some paths are remved in the newer version of the plugin
for (ApiHolder oldHolder : old.holders) {
if (apiInfo.get(oldHolder.api.getEndPoint()) == null) {
// there was a path in the old plugin which is not present in the new one
containerApiBag.unregister(
oldHolder.getMethod(), getActualPath(old, oldHolder.getPath()));
}
}
if (old instanceof Closeable) {
// close the old instance of the plugin
closeWhileHandlingException((Closeable) old);
}
}
}
}
}
}
private static String getActualPath(ApiInfo apiInfo, String path) {
return path.replace("$path-prefix", Objects.requireNonNullElse(apiInfo.info.pathPrefix, ""))
.replace("$plugin-name", apiInfo.info.name);
}
private static Map getTemplateVars(PluginMeta pluginMeta) {
return Utils.makeMap("plugin-name", pluginMeta.name, "path-prefix", pluginMeta.pathPrefix);
}
private static class ApiHolder extends Api {
final AnnotatedApi api;
protected ApiHolder(AnnotatedApi api) {
super(api);
this.api = api;
}
@Override
public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
api.call(req, rsp);
}
public String getPath() {
return api.getEndPoint().path()[0];
}
public SolrRequest.METHOD getMethod() {
return api.getEndPoint().method()[0];
}
}
public class ApiInfo implements ReflectMapWriter {
List holders;
private final PluginMetaHolder holder;
@JsonProperty private final PluginMeta info;
@JsonProperty(value = "package")
public final String pkg;
private SolrPackageLoader.SolrPackage.Version pkgVersion;
private Class klas;
Object instance;
ApiHolder get(EndPoint endPoint) {
for (ApiHolder holder : holders) {
EndPoint e = holder.api.getEndPoint();
if (Objects.equals(endPoint.method()[0], e.method()[0])
&& Objects.equals(endPoint.path()[0], e.path()[0])) {
return holder;
}
}
return null;
}
public Object getInstance() {
return instance;
}
public PluginMeta getInfo() {
return info.copy();
}
public ApiInfo(PluginMetaHolder infoHolder, List errs) {
this.holder = infoHolder;
this.info = infoHolder.meta;
PluginInfo.ClassName klassInfo = new PluginInfo.ClassName(info.klass);
pkg = klassInfo.pkg;
if (pkg != null) {
Optional ver =
coreContainer.getPackageLoader().getPackageVersion(pkg, info.version);
if (ver.isEmpty()) {
// may be we are a bit early. Do a refresh and try again
coreContainer.getPackageLoader().getPackageAPI().refreshPackages(null);
ver = coreContainer.getPackageLoader().getPackageVersion(pkg, info.version);
}
if (ver.isEmpty()) {
SolrPackageLoader.SolrPackage p = coreContainer.getPackageLoader().getPackage(pkg);
if (p == null) {
errs.add("Invalid package " + klassInfo.pkg);
return;
} else {
errs.add(
"No such package version:"
+ pkg
+ ":"
+ info.version
+ " . available versions :"
+ p.allVersions());
return;
}
}
this.pkgVersion = ver.get();
try {
klas = pkgVersion.getLoader().findClass(klassInfo.className, Object.class);
} catch (Exception e) {
log.error("Error loading class", e);
errs.add("Error loading class " + e.toString());
return;
}
} else {
try {
klas = coreContainer.getResourceLoader().findClass(klassInfo.className, Object.class);
} catch (Exception e) {
errs.add(e.toString());
return;
}
pkgVersion = null;
}
if (!Modifier.isPublic(klas.getModifiers())) {
errs.add("Class must be public and static : " + klas.getName());
return;
}
try {
List apis = AnnotatedApi.getApis(klas, null, true);
for (Object api : apis) {
EndPoint endPoint = ((AnnotatedApi) api).getEndPoint();
if (endPoint.path().length > 1 || endPoint.method().length > 1) {
errs.add("Only one HTTP method and url supported for each API");
}
if (endPoint.method().length != 1 || endPoint.path().length != 1) {
errs.add("The @EndPoint must have exactly one method and path attributes");
}
List pathSegments = StrUtils.splitSmart(endPoint.path()[0], '/', true);
PathTrie.replaceTemplates(pathSegments, getTemplateVars(info));
if (V2HttpCall.knownPrefixes.contains(pathSegments.get(0))) {
errs.add("path must not have a prefix: " + pathSegments.get(0));
}
}
} catch (Exception e) {
errs.add(e.toString());
}
if (!errs.isEmpty()) return;
Constructor constructor = klas.getConstructors()[0];
if (constructor.getParameterTypes().length > 1
|| (constructor.getParameterTypes().length == 1
&& constructor.getParameterTypes()[0] != CoreContainer.class)) {
errs.add(
"Must have a no-arg constructor or CoreContainer constructor and it must not be a non static inner class");
return;
}
if (!Modifier.isPublic(constructor.getModifiers())) {
errs.add("Must have a public constructor ");
return;
}
}
@SuppressWarnings({"unchecked"})
public void init() throws Exception {
if (this.holders != null) return;
Constructor constructor = klas.getConstructors()[0];
if (constructor.getParameterTypes().length == 0) {
instance = constructor.newInstance();
} else if (constructor.getParameterTypes().length == 1
&& constructor.getParameterTypes()[0] == CoreContainer.class) {
instance = constructor.newInstance(coreContainer);
} else {
throw new RuntimeException("Must have a no-arg constructor or CoreContainer constructor ");
}
Map config =
(Map) holder.original.getOrDefault("config", Collections.emptyMap());
configure(instance, config, holder.meta);
if (instance instanceof ResourceLoaderAware) {
try {
((ResourceLoaderAware) instance).inform(pkgVersion.getLoader());
} catch (IOException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
}
}
this.holders = new ArrayList<>();
for (Api api : AnnotatedApi.getApis(instance.getClass(), instance, true)) {
holders.add(new ApiHolder((AnnotatedApi) api));
}
}
}
@SuppressWarnings("unchecked")
public static MapWriter configure(Object instance, Map config, PluginMeta meta)
throws IOException {
if (instance instanceof ConfigurablePlugin) {
Class c =
getConfigClass((ConfigurablePlugin) instance);
if (c != null) {
MapWriter configObj = mapper.readValue(Utils.toJSON(config), c);
if (null != meta) {
meta.config = configObj;
}
((ConfigurablePlugin) instance).configure(configObj);
return configObj;
}
}
return null;
}
/** Get the generic type of a {@link ConfigurablePlugin} */
@SuppressWarnings("unchecked")
public static Class getConfigClass(ConfigurablePlugin o) {
Class klas = o.getClass();
do {
Type[] interfaces = klas.getGenericInterfaces();
for (Type type : interfaces) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type rawType = parameterizedType.getRawType();
if (rawType == ConfigurablePlugin.class
||
// or if a super interface is a ConfigurablePlugin
((rawType instanceof Class)
&& ConfigurablePlugin.class.isAssignableFrom((Class) rawType))) {
return (Class) parameterizedType.getActualTypeArguments()[0];
}
}
}
klas = klas.getSuperclass();
} while (klas != null && klas != Object.class);
return null;
}
public ApiInfo createInfo(Map info, List errs) throws IOException {
return new ApiInfo(new PluginMetaHolder(info), errs);
}
public enum Diff {
ADDED,
REMOVED,
UNCHANGED,
UPDATED
}
public static Map compareMaps(Map a, Map b) {
if (a.isEmpty() && b.isEmpty()) return null;
Map result = CollectionUtil.newHashMap(Math.max(a.size(), b.size()));
a.forEach(
(k, v) -> {
Object newVal = b.get(k);
if (newVal == null) {
result.put(k, Diff.REMOVED);
return;
}
result.put(k, Objects.equals(v, newVal) ? Diff.UNCHANGED : Diff.UPDATED);
});
b.forEach(
(k, v) -> {
if (a.get(k) == null) result.put(k, Diff.ADDED);
});
for (Diff value : result.values()) {
if (value != Diff.UNCHANGED) return result;
}
return null;
}
/** Listener for notifications about added / deleted / modified plugins. */
public interface PluginRegistryListener {
/** Called when a new plugin is added. */
void added(ApiInfo plugin);
/** Called when an existing plugin is deleted. */
void deleted(ApiInfo plugin);
/** Called when an existing plugin is replaced. */
void modified(ApiInfo old, ApiInfo replacement);
}
}