// 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 com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; 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.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.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 java.lang.annotation.Annotation; import java.lang.reflect.ParameterizedType; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Tracks Guice bindings that should be exposed to loaded plugins. * <p> * 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<Key<?>> copyConfigKeys; private final List<StartPluginListener> onStart; private final List<StopPluginListener> onStop; private final List<ReloadPluginListener> onReload; private Module sysModule; private Module sshModule; private Module httpModule; private Provider<ModuleGenerator> sshGen; private Provider<ModuleGenerator> httpGen; private Map<TypeLiteral<?>, DynamicItem<?>> sysItems; private Map<TypeLiteral<?>, DynamicItem<?>> sshItems; private Map<TypeLiteral<?>, DynamicItem<?>> httpItems; private Map<TypeLiteral<?>, DynamicSet<?>> sysSets; private Map<TypeLiteral<?>, DynamicSet<?>> sshSets; private Map<TypeLiteral<?>, DynamicSet<?>> httpSets; private Map<TypeLiteral<?>, DynamicMap<?>> sysMaps; private Map<TypeLiteral<?>, DynamicMap<?>> sshMaps; private Map<TypeLiteral<?>, DynamicMap<?>> httpMaps; @Inject PluginGuiceEnvironment( Injector sysInjector, ThreadLocalRequestContext local, ServerInformation srvInfo, CopyConfigModule ccm) { this.sysInjector = sysInjector; this.srvInfo = srvInfo; this.local = local; this.copyConfigModule = ccm; this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet(); 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); } ServerInformation getServerInformation() { return srvInfo; } 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)); } Module getSysModule() { return sysModule; } public void setCfgInjector(Injector cfgInjector) { final Module cm = copy(cfgInjector); final Module sm = copy(sysInjector); sysModule = new AbstractModule() { @Override protected void configure() { install(copyConfigModule); install(cm); install(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 = dynamicSetsOf(injector); httpMaps = dynamicMapsOf(injector); onStart.addAll(listeners(injector, StartPluginListener.class)); onStop.addAll(listeners(injector, StopPluginListener.class)); onReload.addAll(listeners(injector, ReloadPluginListener.class)); } boolean hasHttpModule() { return httpModule != null; } Module getHttpModule() { return httpModule; } ModuleGenerator newHttpModuleGenerator() { return httpGen.get(); } RequestContext enter(Plugin plugin) { return local.setContext(new PluginRequestContext(plugin.getPluginUser())); } void exit(RequestContext old) { local.setContext(old); } 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); } finally { exit(oldContext); } for (StartPluginListener l : onStart) { l.onStartPlugin(plugin); } } void onStopPlugin(Plugin plugin) { for (StopPluginListener l : onStop) { l.onStopPlugin(plugin); } } private void attachItem(Map<TypeLiteral<?>, DynamicItem<?>> items, @Nullable Injector src, Plugin plugin) { for (RegistrationHandle h : PrivateInternals_DynamicTypes .attachItems(src, items, plugin.getName())) { plugin.add(h); } } private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets, @Nullable Injector src, Plugin plugin) { for (RegistrationHandle h : PrivateInternals_DynamicTypes .attachSets(src, sets)) { plugin.add(h); } } private void attachMap(Map<TypeLiteral<?>, 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<TypeLiteral<?>, ReloadableRegistrationHandle<?>> old = LinkedListMultimap.create(); for (ReloadableRegistrationHandle<?> h : oldPlugin.getReloadableHandles()) { old.put(h.getKey().getTypeLiteral(), h); } RequestContext oldContext = enter(newPlugin); try { reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin); reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin); reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin); reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin); reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin); reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin); reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin); reattachItem(old, sshItems, newPlugin.getSshInjector(), newPlugin); reattachItem(old, httpItems, newPlugin.getHttpInjector(), newPlugin); } finally { exit(oldContext); } for (ReloadPluginListener l : onReload) { l.onReloadPlugin(oldPlugin, newPlugin); } } private void reattachMap( ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles, Map<TypeLiteral<?>, DynamicMap<?>> maps, @Nullable Injector src, Plugin newPlugin) { if (src == null || maps == null || maps.isEmpty()) { return; } for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) { @SuppressWarnings("unchecked") TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey(); @SuppressWarnings("unchecked") PrivateInternals_DynamicMapImpl<Object> map = (PrivateInternals_DynamicMapImpl<Object>) e.getValue(); Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap(); 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<Object> b = (Binding<Object>) binding; Key<Object> key = b.getKey(); if (key.getAnnotation() == null) { continue; } @SuppressWarnings("unchecked") ReloadableRegistrationHandle<Object> h = (ReloadableRegistrationHandle<Object>) 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().getClass(); private void reattachSet( ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles, Map<TypeLiteral<?>, DynamicSet<?>> sets, @Nullable Injector src, Plugin newPlugin) { if (src == null || sets == null || sets.isEmpty()) { return; } for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) { @SuppressWarnings("unchecked") TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey(); @SuppressWarnings("unchecked") DynamicSet<Object> set = (DynamicSet<Object>) e.getValue(); // Index all old handles that match this DynamicSet<T> keyed by // annotations. Ignore the unique annotations, thereby favoring // the @Named annotations or some other non-unique naming. Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap(); List<ReloadableRegistrationHandle<?>> old = oldHandles.get(type); Iterator<ReloadableRegistrationHandle<?>> 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<Object> b = (Binding<Object>) binding; Key<Object> key = b.getKey(); if (key.getAnnotation() == null) { continue; } @SuppressWarnings("unchecked") ReloadableRegistrationHandle<Object> h1 = (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation()); if (h1 != null) { replace(newPlugin, h1, b); } else if (oi.hasNext()) { @SuppressWarnings("unchecked") ReloadableRegistrationHandle<Object> h2 = (ReloadableRegistrationHandle<Object>) oi.next(); oi.remove(); replace(newPlugin, h2, b); } else { newPlugin.add(set.add(b.getKey(), b.getProvider())); } } } } private void reattachItem( ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles, Map<TypeLiteral<?>, DynamicItem<?>> items, @Nullable Injector src, Plugin newPlugin) { if (src == null || items == null || items.isEmpty()) { return; } for (Map.Entry<TypeLiteral<?>, DynamicItem<?>> e : items.entrySet()) { @SuppressWarnings("unchecked") TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey(); @SuppressWarnings("unchecked") DynamicItem<Object> item = (DynamicItem<Object>) e.getValue(); Iterator<ReloadableRegistrationHandle<?>> oi = oldHandles.get(type).iterator(); for (Binding<?> binding : bindings(src, type)) { @SuppressWarnings("unchecked") Binding<Object> b = (Binding<Object>) binding; if (oi.hasNext()) { @SuppressWarnings("unchecked") ReloadableRegistrationHandle<Object> h = (ReloadableRegistrationHandle<Object>) oi.next(); oi.remove(); replace(newPlugin, h, b); } else { newPlugin.add(item.set(b.getKey(), b.getProvider(), newPlugin.getName())); } } } } private static <T> void replace(Plugin newPlugin, ReloadableRegistrationHandle<T> h, Binding<T> b) { RegistrationHandle n = h.replace(b.getKey(), b.getProvider()); if (n != null){ newPlugin.add(n); } } static <T> List<T> listeners(Injector src, Class<T> type) { List<Binding<T>> bindings = bindings(src, TypeLiteral.get(type)); int cnt = bindings != null ? bindings.size() : 0; List<T> found = Lists.newArrayListWithCapacity(cnt); if (bindings != null) { for (Binding<T> b : bindings) { found.add(b.getProvider().get()); } } return found; } private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) { return src.findBindingsByType(type); } private Module copy(Injector src) { Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet(); Set<TypeLiteral<?>> dynamicItemTypes = Sets.newHashSet(); for (Map.Entry<Key<?>, 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<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap(); for (Map.Entry<Key<?>, 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<F> or DynamicMap<F> 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)); final @Nullable Binding<HttpServletRequest> requestBinding = src.getExistingBinding(Key.get(HttpServletRequest.class)); final @Nullable Binding<HttpServletResponse> responseBinding = src.getExistingBinding(Key.get(HttpServletResponse.class)); return new AbstractModule() { @SuppressWarnings("unchecked") @Override protected void configure() { for (Map.Entry<Key<?>, Binding<?>> e : bindings.entrySet()) { Key<Object> k = (Key<Object>) e.getKey(); Binding<Object> b = (Binding<Object>) 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)) { return false; } if (StartPluginListener.class.isAssignableFrom(type)) { return false; } if (StopPluginListener.class.isAssignableFrom(type)) { return false; } if (type.getName().startsWith("com.google.inject.")) { return false; } if (is("org.apache.sshd.server.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; } }