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

com.google.gerrit.server.plugins.PluginGuiceEnvironment Maven / Gradle / Ivy

The newest version!
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.server.plugins;

import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicItemsOf;
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.extensions.annotations.RootRelative;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
import com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
import com.google.gerrit.extensions.webui.WebUiPlugin;
import com.google.gerrit.index.IndexCollection;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.util.PluginRequestContext;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.util.Modules;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Tracks Guice bindings that should be exposed to loaded plugins.
 *
 * 

This is an internal implementation detail of how the main server is able to export its * explicit Guice bindings to tightly coupled plugins, giving them access to singletons and request * scoped resources just like any core code. */ @Singleton public class PluginGuiceEnvironment { private final Injector sysInjector; private final ServerInformation srvInfo; private final ThreadLocalRequestContext local; private final CopyConfigModule copyConfigModule; private final Set> copyConfigKeys; private final List onStart; private final List onStop; private final List onReload; private final MetricMaker serverMetrics; private Module sysModule; private Module sshModule; private Module httpModule; private Injector apiInjector; private Provider sshGen; private Provider httpGen; private Map, DynamicItem> sysItems; private Map, DynamicItem> sshItems; private Map, DynamicItem> httpItems; private Map, DynamicItem> apiItems; private Map, DynamicSet> sysSets; private Map, DynamicSet> sshSets; private Map, DynamicSet> httpSets; private Map, DynamicSet> apiSets; private Map, DynamicMap> sysMaps; private Map, DynamicMap> sshMaps; private Map, DynamicMap> httpMaps; private Map, DynamicMap> apiMaps; @Inject PluginGuiceEnvironment( Injector sysInjector, ThreadLocalRequestContext local, ServerInformation srvInfo, CopyConfigModule ccm, MetricMaker serverMetrics) { this.sysInjector = sysInjector; this.srvInfo = srvInfo; this.local = local; this.copyConfigModule = ccm; this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet(); this.serverMetrics = serverMetrics; onStart = new CopyOnWriteArrayList<>(); onStart.addAll(listeners(sysInjector, StartPluginListener.class)); onStop = new CopyOnWriteArrayList<>(); onStop.addAll(listeners(sysInjector, StopPluginListener.class)); onReload = new CopyOnWriteArrayList<>(); onReload.addAll(listeners(sysInjector, ReloadPluginListener.class)); sysItems = dynamicItemsOf(sysInjector); sysSets = dynamicSetsOf(sysInjector); sysMaps = dynamicMapsOf(sysInjector); apiSets = new HashMap<>(); apiItems = new HashMap<>(); apiMaps = new HashMap<>(); } ServerInformation getServerInformation() { return srvInfo; } MetricMaker getServerMetrics() { return serverMetrics; } boolean hasDynamicItem(TypeLiteral type) { return sysItems.containsKey(type) || (sshItems != null && sshItems.containsKey(type)) || (httpItems != null && httpItems.containsKey(type)); } boolean hasDynamicSet(TypeLiteral type) { return sysSets.containsKey(type) || (sshSets != null && sshSets.containsKey(type)) || (httpSets != null && httpSets.containsKey(type)); } boolean hasDynamicMap(TypeLiteral type) { return sysMaps.containsKey(type) || (sshMaps != null && sshMaps.containsKey(type)) || (httpMaps != null && httpMaps.containsKey(type)); } public Module getSysModule() { return sysModule; } public void setDbCfgInjector(Injector dbInjector, Injector cfgInjector) { final Module db = copy(dbInjector); final Module cm = copy(cfgInjector); final Module sm = copy(sysInjector); sysModule = Modules.combine(copyConfigModule, db, cm, sm); } public void setSshInjector(Injector injector) { sshModule = copy(injector); sshGen = injector.getProvider(ModuleGenerator.class); sshItems = dynamicItemsOf(injector); sshSets = dynamicSetsOf(injector); sshMaps = dynamicMapsOf(injector); onStart.addAll(listeners(injector, StartPluginListener.class)); onStop.addAll(listeners(injector, StopPluginListener.class)); onReload.addAll(listeners(injector, ReloadPluginListener.class)); } boolean hasSshModule() { return sshModule != null; } Module getSshModule() { return sshModule; } ModuleGenerator newSshModuleGenerator() { return sshGen.get(); } public void setHttpInjector(Injector injector) { httpModule = copy(injector); httpGen = injector.getProvider(ModuleGenerator.class); httpItems = dynamicItemsOf(injector); httpSets = httpDynamicSetsOf(injector); httpMaps = dynamicMapsOf(injector); onStart.addAll(listeners(injector, StartPluginListener.class)); onStop.addAll(listeners(injector, StopPluginListener.class)); onReload.addAll(listeners(injector, ReloadPluginListener.class)); } private Map, DynamicSet> httpDynamicSetsOf(Injector i) { // Copy binding of DynamicSet from sysInjector to HTTP. // This supports older plugins that bound a plugin in the HttpModule. TypeLiteral key = TypeLiteral.get(WebUiPlugin.class); DynamicSet web = sysSets.get(key); requireNonNull(web, "DynamicSet exists in sysInjector"); Map, DynamicSet> m = new HashMap<>(dynamicSetsOf(i)); m.put(key, web); return Collections.unmodifiableMap(m); } boolean hasHttpModule() { return httpModule != null; } @UsedAt(UsedAt.Project.GOOGLE) public Module getHttpModule() { return httpModule; } ModuleGenerator newHttpModuleGenerator() { return httpGen.get(); } public RequestContext enter(Plugin plugin) { return local.setContext(new PluginRequestContext(plugin.getPluginUser())); } public void exit(RequestContext old) { @SuppressWarnings("unused") var unused = local.setContext(old); } public void onStartPlugin(Plugin plugin) { RequestContext oldContext = enter(plugin); try { attachItem(sysItems, plugin.getSysInjector(), plugin); attachItem(sshItems, plugin.getSshInjector(), plugin); attachItem(httpItems, plugin.getHttpInjector(), plugin); attachSet(sysSets, plugin.getSysInjector(), plugin); attachSet(sshSets, plugin.getSshInjector(), plugin); attachSet(httpSets, plugin.getHttpInjector(), plugin); attachMap(sysMaps, plugin.getSysInjector(), plugin); attachMap(sshMaps, plugin.getSshInjector(), plugin); attachMap(httpMaps, plugin.getHttpInjector(), plugin); apiInjector = Optional.ofNullable(plugin.getApiInjector()).orElse(apiInjector); if (apiInjector != null) { apiItems.putAll(dynamicItemsOf(apiInjector)); apiSets.putAll(dynamicSetsOf(apiInjector)); apiMaps.putAll(dynamicMapsOf(apiInjector)); ImmutableList allPluginInjectors = listOfInjectors( plugin.getSysInjector(), plugin.getSshInjector(), plugin.getHttpInjector()); allPluginInjectors.forEach(i -> attachItem(apiItems, i, plugin)); allPluginInjectors.forEach(i -> attachSet(apiSets, i, plugin)); allPluginInjectors.forEach(i -> attachMap(apiMaps, i, plugin)); } } finally { exit(oldContext); } for (StartPluginListener l : onStart) { l.onStartPlugin(plugin); } } private ImmutableList listOfInjectors(Injector... injectors) { ImmutableList.Builder injectorsListBuilder = ImmutableList.builder(); for (Injector injector : injectors) { if (injector != null) { injectorsListBuilder.add(injector); } } return injectorsListBuilder.build(); } public void onStopPlugin(Plugin plugin) { for (StopPluginListener l : onStop) { l.onStopPlugin(plugin); } } private void attachItem( Map, DynamicItem> items, @Nullable Injector src, Plugin plugin) { for (RegistrationHandle h : PrivateInternals_DynamicTypes.attachItems(src, plugin.getName(), items)) { plugin.add(h); } } private void attachSet( Map, DynamicSet> sets, @Nullable Injector src, Plugin plugin) { for (RegistrationHandle h : PrivateInternals_DynamicTypes.attachSets(src, plugin.getName(), sets)) { plugin.add(h); } } private void attachMap( Map, DynamicMap> maps, @Nullable Injector src, Plugin plugin) { for (RegistrationHandle h : PrivateInternals_DynamicTypes.attachMaps(src, plugin.getName(), maps)) { plugin.add(h); } } void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) { // Index all old registrations by the raw type. These may be replaced // during the reattach calls below. Any that are not replaced will be // removed when the old plugin does its stop routine. ListMultimap, ReloadableRegistrationHandle> old = LinkedListMultimap.create(); for (ReloadableRegistrationHandle h : oldPlugin.getReloadableHandles()) { old.put(h.getKey().getTypeLiteral(), h); } RequestContext oldContext = enter(newPlugin); try { Optional.ofNullable(newPlugin.getApiInjector()) .ifPresent(i -> reattachMap(old, apiMaps, i, newPlugin)); reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin); reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin); reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin); Optional.ofNullable(newPlugin.getApiInjector()) .ifPresent(i -> reattachSet(old, apiSets, i, newPlugin)); reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin); reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin); reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin); Optional.ofNullable(newPlugin.getApiInjector()) .ifPresent(i -> reattachItem(old, apiItems, i, newPlugin)); reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin); reattachItem(old, sshItems, newPlugin.getSshInjector(), newPlugin); reattachItem(old, httpItems, newPlugin.getHttpInjector(), newPlugin); apiInjector = Optional.ofNullable(newPlugin.getApiInjector()).orElse(apiInjector); if (apiInjector != null) { apiItems.putAll(dynamicItemsOf(apiInjector)); apiSets.putAll(dynamicSetsOf(apiInjector)); apiMaps.putAll(dynamicMapsOf(apiInjector)); ImmutableList allPluginInjectors = listOfInjectors( newPlugin.getSysInjector(), newPlugin.getSshInjector(), newPlugin.getHttpInjector()); allPluginInjectors.forEach(i -> reattachItem(old, apiItems, i, newPlugin)); allPluginInjectors.forEach(i -> reattachSet(old, apiSets, i, newPlugin)); allPluginInjectors.forEach(i -> reattachMap(old, apiMaps, i, newPlugin)); } } finally { exit(oldContext); } for (ReloadPluginListener l : onReload) { l.onReloadPlugin(oldPlugin, newPlugin); } } private void reattachMap( ListMultimap, ReloadableRegistrationHandle> oldHandles, Map, DynamicMap> maps, @Nullable Injector src, Plugin newPlugin) { if (src == null || maps == null || maps.isEmpty()) { return; } for (Map.Entry, DynamicMap> e : maps.entrySet()) { @SuppressWarnings("unchecked") TypeLiteral type = (TypeLiteral) e.getKey(); @SuppressWarnings("unchecked") PrivateInternals_DynamicMapImpl map = (PrivateInternals_DynamicMapImpl) e.getValue(); Map> am = new HashMap<>(); for (ReloadableRegistrationHandle h : oldHandles.get(type)) { Annotation a = h.getKey().getAnnotation(); if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) { am.put(a, h); } } for (Binding binding : bindings(src, e.getKey())) { @SuppressWarnings("unchecked") Binding b = (Binding) binding; Key key = b.getKey(); if (key.getAnnotation() == null) { continue; } @SuppressWarnings("unchecked") ReloadableRegistrationHandle h = (ReloadableRegistrationHandle) am.remove(key.getAnnotation()); if (h != null) { replace(newPlugin, h, b); oldHandles.remove(type, h); } else { newPlugin.add(map.put(newPlugin.getName(), b.getKey(), b.getProvider())); } } } } /** Type used to declare unique annotations. Guice hides this, so extract it. */ private static final Class UNIQUE_ANNOTATION = UniqueAnnotations.create().annotationType(); private void reattachSet( ListMultimap, ReloadableRegistrationHandle> oldHandles, Map, DynamicSet> sets, @Nullable Injector src, Plugin newPlugin) { if (src == null || sets == null || sets.isEmpty()) { return; } for (Map.Entry, DynamicSet> e : sets.entrySet()) { @SuppressWarnings("unchecked") TypeLiteral type = (TypeLiteral) e.getKey(); @SuppressWarnings("unchecked") DynamicSet set = (DynamicSet) e.getValue(); // Index all old handles that match this DynamicSet keyed by // annotations. Ignore the unique annotations, thereby favoring // the @Named annotations or some other non-unique naming. Map> am = new HashMap<>(); List> old = oldHandles.get(type); Iterator> oi = old.iterator(); while (oi.hasNext()) { ReloadableRegistrationHandle h = oi.next(); Annotation a = h.getKey().getAnnotation(); if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) { am.put(a, h); oi.remove(); } } // Replace old handles with new bindings, favoring cases where there // is an exact match on an @Named annotation. If there is no match // pick any handle and replace it. We generally expect only one // handle of each DynamicSet type when using unique annotations, but // possibly multiple ones if @Named was used. Plugin authors that want // atomic replacement across reloads should use @Named annotations with // stable names that do not change across plugin versions to ensure the // handles are swapped correctly. oi = old.iterator(); for (Binding binding : bindings(src, type)) { @SuppressWarnings("unchecked") Binding b = (Binding) binding; Key key = b.getKey(); if (key.getAnnotation() == null) { continue; } @SuppressWarnings("unchecked") ReloadableRegistrationHandle h1 = (ReloadableRegistrationHandle) am.remove(key.getAnnotation()); if (h1 != null) { replace(newPlugin, h1, b); } else if (oi.hasNext()) { @SuppressWarnings("unchecked") ReloadableRegistrationHandle h2 = (ReloadableRegistrationHandle) oi.next(); oi.remove(); replace(newPlugin, h2, b); } else { newPlugin.add(set.add(newPlugin.getName(), b.getKey(), b.getProvider())); } } } } private void reattachItem( ListMultimap, ReloadableRegistrationHandle> oldHandles, Map, DynamicItem> items, @Nullable Injector src, Plugin newPlugin) { if (src == null || items == null || items.isEmpty()) { return; } for (Map.Entry, DynamicItem> e : items.entrySet()) { @SuppressWarnings("unchecked") TypeLiteral type = (TypeLiteral) e.getKey(); @SuppressWarnings("unchecked") DynamicItem item = (DynamicItem) e.getValue(); Iterator> oi = oldHandles.get(type).iterator(); for (Binding binding : bindings(src, type)) { @SuppressWarnings("unchecked") Binding b = (Binding) binding; if (oi.hasNext()) { @SuppressWarnings("unchecked") ReloadableRegistrationHandle h = (ReloadableRegistrationHandle) oi.next(); oi.remove(); replace(newPlugin, h, b); } else { newPlugin.add(item.set(b.getKey(), b.getProvider(), newPlugin.getName())); } } } } private static void replace( Plugin newPlugin, ReloadableRegistrationHandle h, Binding b) { RegistrationHandle n = h.replace(b.getKey(), b.getProvider()); if (n != null) { newPlugin.add(n); } } static List listeners(Injector src, Class type) { List> bindings = bindings(src, TypeLiteral.get(type)); int cnt = bindings != null ? bindings.size() : 0; List found = Lists.newArrayListWithCapacity(cnt); if (bindings != null) { for (Binding b : bindings) { found.add(b.getProvider().get()); } } return found; } private static List> bindings(Injector src, TypeLiteral type) { return src.findBindingsByType(type); } private Module copy(Injector src) { Set> dynamicTypes = new HashSet<>(); Set> dynamicItemTypes = new HashSet<>(); for (Map.Entry, Binding> e : src.getBindings().entrySet()) { TypeLiteral type = e.getKey().getTypeLiteral(); if (type.getRawType() == DynamicItem.class) { ParameterizedType t = (ParameterizedType) type.getType(); dynamicItemTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0])); } else if (type.getRawType() == DynamicSet.class || type.getRawType() == DynamicMap.class) { ParameterizedType t = (ParameterizedType) type.getType(); dynamicTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0])); } } final Map, Binding> bindings = new LinkedHashMap<>(); for (Map.Entry, Binding> e : src.getBindings().entrySet()) { if (dynamicTypes.contains(e.getKey().getTypeLiteral()) && e.getKey().getAnnotation() != null) { // A type used in DynamicSet or DynamicMap that has an annotation // must be picked up by the set/map itself. A type used in either // but without an annotation may be magic glue implementing F and // using DynamicSet or DynamicMap internally. That should be // exported to plugins. continue; } else if (dynamicItemTypes.contains(e.getKey().getTypeLiteral())) { continue; } else if (shouldCopy(e.getKey())) { bindings.put(e.getKey(), e.getValue()); } } bindings.remove(Key.get(Injector.class)); bindings.remove(Key.get(java.util.logging.Logger.class)); @Nullable final Binding requestBinding = src.getExistingBinding(Key.get(HttpServletRequest.class)); @Nullable final Binding responseBinding = src.getExistingBinding(Key.get(HttpServletResponse.class)); return new AbstractModule() { @SuppressWarnings("unchecked") @Override protected void configure() { for (Map.Entry, Binding> e : bindings.entrySet()) { Key k = (Key) e.getKey(); Binding b = (Binding) e.getValue(); bind(k).toProvider(b.getProvider()); } if (requestBinding != null) { bind(HttpServletRequest.class) .annotatedWith(RootRelative.class) .toProvider(requestBinding.getProvider()); } if (responseBinding != null) { bind(HttpServletResponse.class) .annotatedWith(RootRelative.class) .toProvider(responseBinding.getProvider()); } } }; } private boolean shouldCopy(Key key) { if (copyConfigKeys.contains(key)) { return false; } Class type = key.getTypeLiteral().getRawType(); if (LifecycleListener.class.isAssignableFrom(type) // This is needed for secondary index to work from plugin listeners && !IndexCollection.class.isAssignableFrom(type)) { return false; } if (StartPluginListener.class.isAssignableFrom(type)) { return false; } if (StopPluginListener.class.isAssignableFrom(type)) { return false; } if (MetricMaker.class.isAssignableFrom(type)) { return false; } if (type.getName().startsWith("com.google.inject.")) { return false; } if (is("org.apache.sshd.server.command.Command", type)) { return false; } if (is("javax.servlet.Filter", type)) { return false; } if (is("javax.servlet.ServletContext", type)) { return false; } if (is("javax.servlet.ServletRequest", type)) { return false; } if (is("javax.servlet.ServletResponse", type)) { return false; } if (is("javax.servlet.http.HttpServlet", type)) { return false; } if (is("javax.servlet.http.HttpServletRequest", type)) { return false; } if (is("javax.servlet.http.HttpServletResponse", type)) { return false; } if (is("javax.servlet.http.HttpSession", type)) { return false; } if (Map.class.isAssignableFrom(type) && key.getAnnotationType() != null && "com.google.inject.servlet.RequestParameters" .equals(key.getAnnotationType().getName())) { return false; } if (type.getName().startsWith("com.google.gerrit.httpd.GitOverHttpServlet$")) { return false; } return true; } static boolean is(String name, Class type) { while (type != null) { if (name.equals(type.getName())) { return true; } Class[] interfaces = type.getInterfaces(); if (interfaces != null) { for (Class i : interfaces) { if (is(name, i)) { return true; } } } type = type.getSuperclass(); } return false; } @Nullable public Injector getApiInjector() { return apiInjector; } }