diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java new file mode 100644 index 0000000000..aa1dc76126 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java @@ -0,0 +1,220 @@ +// 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.extensions.registration; + +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Scopes; +import com.google.inject.TypeLiteral; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.util.Providers; +import com.google.inject.util.Types; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * A single item that can be modified as plugins reload. + *

+ * DynamicItems are always mapped as singletons in Guice. Items store a Provider + * internally, and resolve the provider to an instance on demand. This enables + * registrations to decide between singleton and non-singleton members. If + * multiple plugins try to provide the same Provider, an exception is thrown. + */ +public class DynamicItem { + /** Pair of provider implementation and plugin providing it. */ + static class NamedProvider { + final Provider impl; + final String pluginName; + + NamedProvider(Provider provider, String pluginName) { + this.impl = provider; + this.pluginName = pluginName; + } + } + + /** + * Declare a singleton {@code DynamicItem} with a binder. + *

+ * Items must be defined in a Guice module before they can be bound: + *

+   *   DynamicItem.itemOf(binder(), Interface.class);
+   *   DynamicItem.bind(binder(), Interface.class).to(Impl.class);
+   * 
+ * + * @param binder a new binder created in the module. + * @param member type of entry to store. + */ + public static void itemOf(Binder binder, Class member) { + itemOf(binder, TypeLiteral.get(member)); + } + + /** + * Declare a singleton {@code DynamicItem} with a binder. + *

+ * Items must be defined in a Guice module before they can be bound: + *

+   *   DynamicSet.itemOf(binder(), new TypeLiteral>() {});
+   * 
+ * + * @param binder a new binder created in the module. + * @param member type of entry to store. + */ + public static void itemOf(Binder binder, TypeLiteral member) { + @SuppressWarnings("unchecked") + Key> key = (Key>) Key.get( + Types.newParameterizedType(DynamicItem.class, member.getType())); + binder.bind(key) + .toProvider(new DynamicItemProvider(member, key)) + .in(Scopes.SINGLETON); + } + + /** + * Bind one implementation as the item using a unique annotation. + * + * @param binder a new binder created in the module. + * @param type type of entry to store. + * @return a binder to continue configuring the new item. + */ + public static LinkedBindingBuilder bind(Binder binder, Class type) { + return bind(binder, TypeLiteral.get(type)); + } + + /** + * Bind one implementation as the item. + * + * @param binder a new binder created in the module. + * @param type type of entry to store. + * @return a binder to continue configuring the new item. + */ + public static LinkedBindingBuilder bind(Binder binder, + TypeLiteral type) { + return binder.bind(type); + } + + private final Key> key; + private final AtomicReference> ref; + + DynamicItem(Key> key, Provider provider, String pluginName) { + NamedProvider in = null; + if (provider != null) { + in = new NamedProvider(provider, pluginName); + } + this.key = key; + this.ref = new AtomicReference>(in); + } + + /** + * Get the configured item, or null. + * + * @return the configured item instance; null if no implementation has been + * bound to the item. This is common if no plugin registered an + * implementation for the type. + */ + public T get() { + NamedProvider item = ref.get(); + return item != null ? item.impl.get() : null; + } + + /** + * Set the element to provide. + * + * @param item the item to use. Must not be null. + * @param pluginName the name of the plugin providing the item. + * @return handle to remove the item at a later point in time. + */ + public RegistrationHandle set(T item, String pluginName) { + return set(Providers.of(item), pluginName); + } + + /** + * Set the element to provide. + * + * @param impl the item to add to the collection. Must not be null. + * @param pluginName name of the source providing the implementation. + * @return handle to remove the item at a later point in time. + */ + public RegistrationHandle set(Provider impl, String pluginName) { + final NamedProvider item = new NamedProvider(impl, pluginName); + while (!ref.compareAndSet(null, item)) { + NamedProvider old = ref.get(); + if (old != null) { + throw new ProvisionException(String.format( + "%s already provided by %s, ignoring plugin %s", + key.getTypeLiteral(), old.pluginName, pluginName)); + } + } + return new RegistrationHandle() { + @Override + public void remove() { + ref.compareAndSet(item, null); + } + }; + } + + /** + * Set the element that may be hot-replaceable in the future. + * + * @param key unique description from the item's Guice binding. This can be + * later obtained from the registration handle to facilitate matching + * with the new equivalent instance during a hot reload. + * @param impl the item to set as our value right now. Must not be null. + * @param pluginName the name of the plugin providing the item. + * @return a handle that can remove this item later, or hot-swap the item. + */ + public ReloadableRegistrationHandle set(Key key, Provider impl, + String pluginName) { + final NamedProvider item = new NamedProvider(impl, pluginName); + while (!ref.compareAndSet(null, item)) { + NamedProvider old = ref.get(); + if (old != null) { + throw new ProvisionException(String.format( + "%s already provided by %s, ignoring plugin %s", + this.key.getTypeLiteral(), old.pluginName, pluginName)); + } + } + return new ReloadableHandle(key, item); + } + + private class ReloadableHandle implements ReloadableRegistrationHandle { + private final Key key; + private final NamedProvider item; + + ReloadableHandle(Key key, NamedProvider item) { + this.key = key; + this.item = item; + } + + @Override + public Key getKey() { + return key; + } + + @Override + public void remove() { + ref.compareAndSet(item, null); + } + + @Override + public ReloadableHandle replace(Key newKey, Provider newItem) { + NamedProvider n = new NamedProvider(newItem, item.pluginName); + if (ref.compareAndSet(item, n)) { + return new ReloadableHandle(newKey, n); + } + return null; + } + } +} diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java new file mode 100644 index 0000000000..1074ee5960 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java @@ -0,0 +1,56 @@ +// 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.extensions.registration; + +import com.google.inject.Binding; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.TypeLiteral; + +import java.util.List; + +class DynamicItemProvider implements Provider> { + private final TypeLiteral type; + private final Key> key; + + @Inject + private Injector injector; + + DynamicItemProvider(TypeLiteral type, Key> key) { + this.type = type; + this.key = key; + } + + public DynamicItem get() { + return new DynamicItem(key, find(injector, type), "gerrit"); + } + + private static Provider find(Injector src, TypeLiteral type) { + List> bindings = src.findBindingsByType(type); + if (bindings != null && bindings.size() == 1) { + return bindings.get(0).getProvider(); + } else if (bindings != null && bindings.size() > 1) { + throw new ProvisionException(String.format( + "Multiple providers bound for DynamicItem<%s>\n" + + "This is not allowed; check the server configuration.", + type)); + } else { + return null; + } + } +} diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java index 66dd45d657..8bc57abc91 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java @@ -30,6 +30,22 @@ import java.util.Map; /** DO NOT USE */ public class PrivateInternals_DynamicTypes { + public static Map, DynamicItem> dynamicItemsOf(Injector src) { + Map, DynamicItem> m = newHashMap(); + for (Map.Entry, Binding> e : src.getBindings().entrySet()) { + TypeLiteral type = e.getKey().getTypeLiteral(); + if (type.getRawType() == DynamicItem.class) { + ParameterizedType p = (ParameterizedType) type.getType(); + m.put(TypeLiteral.get(p.getActualTypeArguments()[0]), + (DynamicItem) e.getValue().getProvider().get()); + } + } + if (m.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(m); + } + public static Map, DynamicSet> dynamicSetsOf(Injector src) { Map, DynamicSet> m = newHashMap(); for (Map.Entry, Binding> e : src.getBindings().entrySet()) { @@ -62,6 +78,38 @@ public class PrivateInternals_DynamicTypes { return Collections.unmodifiableMap(m); } + public static List attachItems( + Injector src, + Map, DynamicItem> items, String pluginName) { + if (src == null || items == null || items.isEmpty()) { + return Collections.emptyList(); + } + + List handles = new ArrayList(4); + try { + for (Map.Entry, DynamicItem> e : items.entrySet()) { + @SuppressWarnings("unchecked") + TypeLiteral type = (TypeLiteral) e.getKey(); + + @SuppressWarnings("unchecked") + DynamicItem item = (DynamicItem) e.getValue(); + + for (Binding b : bindings(src, type)) { + if (b.getKey().getAnnotation() != null) { + handles.add(item.set(b.getKey(), b.getProvider(), pluginName)); + } + } + } + } catch (RuntimeException e) { + remove(handles); + throw e; + } catch (Error e) { + remove(handles); + throw e; + } + return handles; + } + public static List attachSets( Injector src, Map, DynamicSet> sets) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java index c2c23a3a0d..6062ae93eb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java @@ -236,7 +236,12 @@ class AutoRegisterModules { if (rawType.getAnnotation(ExtensionPoint.class) != null) { TypeLiteral tl = TypeLiteral.get(type); - if (env.hasDynamicSet(tl)) { + if (env.hasDynamicItem(tl)) { + sysSingletons.add(clazz); + sysListen.put(tl, clazz); + httpGen.listen(tl, clazz); + sshGen.listen(tl, clazz); + } else if (env.hasDynamicSet(tl)) { sysSingletons.add(clazz); sysListen.put(tl, clazz); httpGen.listen(tl, clazz); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java index 18460ff1ff..664c278a19 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java @@ -13,6 +13,8 @@ // 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; @@ -22,6 +24,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; 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; @@ -74,6 +77,8 @@ public class PluginGuiceEnvironment { private Provider sshGen; private Provider httpGen; + private Map, DynamicItem> sysItems; + private Map, DynamicSet> sysSets; private Map, DynamicSet> sshSets; private Map, DynamicSet> httpSets; @@ -98,6 +103,7 @@ public class PluginGuiceEnvironment { onReload = new CopyOnWriteArrayList(); onReload.addAll(listeners(sysInjector, ReloadPluginListener.class)); + sysItems = dynamicItemsOf(sysInjector); sysSets = dynamicSetsOf(sysInjector); sysMaps = dynamicMapsOf(sysInjector); } @@ -106,6 +112,10 @@ public class PluginGuiceEnvironment { return srvInfo; } + boolean hasDynamicItem(TypeLiteral type) { + return sysItems.containsKey(type); + } + boolean hasDynamicSet(TypeLiteral type) { return sysSets.containsKey(type) || (sshSets != null && sshSets.containsKey(type)) @@ -182,6 +192,8 @@ public class PluginGuiceEnvironment { l.onStartPlugin(plugin); } + attachItem(sysItems, plugin.getSysInjector(), plugin); + attachSet(sysSets, plugin.getSysInjector(), plugin); attachSet(sshSets, plugin.getSshInjector(), plugin); attachSet(httpSets, plugin.getHttpInjector(), plugin); @@ -191,6 +203,15 @@ public class PluginGuiceEnvironment { attachMap(httpMaps, plugin.getHttpInjector(), plugin); } + private void attachItem(Map, DynamicItem> items, + @Nullable Injector src, + Plugin plugin) { + for (RegistrationHandle h : PrivateInternals_DynamicTypes + .attachItems(src, items, plugin.getName())) { + plugin.add(h); + } + } + private void attachSet(Map, DynamicSet> sets, @Nullable Injector src, Plugin plugin) { @@ -230,6 +251,8 @@ public class PluginGuiceEnvironment { reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin); reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin); reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin); + + reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin); } private void reattachMap( @@ -350,6 +373,41 @@ public class PluginGuiceEnvironment { } } } + 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) {