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