com.google.gerrit.server.plugins.PluginGuiceEnvironment Maven / Gradle / Ivy
// 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