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:
+ *
+ *
+ * @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