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

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

There is a newer version: 9.7.0
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 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy