Add DynamicItem for extension points with one implementation

Some extension points should only have one implementation provided,
such as a provider of avatar images.  In this case it doesn't make
sense to have multiple providers.  For this use-case we need
DynamicItem.

Change-Id: If5166c3f42c5619daea56d79dea8feb70f9ee084
This commit is contained in:
Brad Larson
2012-12-12 14:09:24 -06:00
committed by Shawn Pearce
parent 095a25143c
commit ae984a98e2
5 changed files with 388 additions and 1 deletions

View File

@@ -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.
* <p>
* 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<T> {
/** Pair of provider implementation and plugin providing it. */
static class NamedProvider<T> {
final Provider<T> impl;
final String pluginName;
NamedProvider(Provider<T> provider, String pluginName) {
this.impl = provider;
this.pluginName = pluginName;
}
}
/**
* Declare a singleton {@code DynamicItem<T>} with a binder.
* <p>
* Items must be defined in a Guice module before they can be bound:
* <pre>
* DynamicItem.itemOf(binder(), Interface.class);
* DynamicItem.bind(binder(), Interface.class).to(Impl.class);
* </pre>
*
* @param binder a new binder created in the module.
* @param member type of entry to store.
*/
public static <T> void itemOf(Binder binder, Class<T> member) {
itemOf(binder, TypeLiteral.get(member));
}
/**
* Declare a singleton {@code DynamicItem<T>} with a binder.
* <p>
* Items must be defined in a Guice module before they can be bound:
* <pre>
* DynamicSet.itemOf(binder(), new TypeLiteral<Thing<Foo>>() {});
* </pre>
*
* @param binder a new binder created in the module.
* @param member type of entry to store.
*/
public static <T> void itemOf(Binder binder, TypeLiteral<T> member) {
@SuppressWarnings("unchecked")
Key<DynamicItem<T>> key = (Key<DynamicItem<T>>) Key.get(
Types.newParameterizedType(DynamicItem.class, member.getType()));
binder.bind(key)
.toProvider(new DynamicItemProvider<T>(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 <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> 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 <T> LinkedBindingBuilder<T> bind(Binder binder,
TypeLiteral<T> type) {
return binder.bind(type);
}
private final Key<DynamicItem<T>> key;
private final AtomicReference<NamedProvider<T>> ref;
DynamicItem(Key<DynamicItem<T>> key, Provider<T> provider, String pluginName) {
NamedProvider<T> in = null;
if (provider != null) {
in = new NamedProvider<T>(provider, pluginName);
}
this.key = key;
this.ref = new AtomicReference<NamedProvider<T>>(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<T> 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<T> impl, String pluginName) {
final NamedProvider<T> item = new NamedProvider<T>(impl, pluginName);
while (!ref.compareAndSet(null, item)) {
NamedProvider<T> 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<T> set(Key<T> key, Provider<T> impl,
String pluginName) {
final NamedProvider<T> item = new NamedProvider<T>(impl, pluginName);
while (!ref.compareAndSet(null, item)) {
NamedProvider<T> 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<T> {
private final Key<T> key;
private final NamedProvider<T> item;
ReloadableHandle(Key<T> key, NamedProvider<T> item) {
this.key = key;
this.item = item;
}
@Override
public Key<T> getKey() {
return key;
}
@Override
public void remove() {
ref.compareAndSet(item, null);
}
@Override
public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
NamedProvider<T> n = new NamedProvider<T>(newItem, item.pluginName);
if (ref.compareAndSet(item, n)) {
return new ReloadableHandle(newKey, n);
}
return null;
}
}
}

View File

@@ -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<T> implements Provider<DynamicItem<T>> {
private final TypeLiteral<T> type;
private final Key<DynamicItem<T>> key;
@Inject
private Injector injector;
DynamicItemProvider(TypeLiteral<T> type, Key<DynamicItem<T>> key) {
this.type = type;
this.key = key;
}
public DynamicItem<T> get() {
return new DynamicItem<T>(key, find(injector, type), "gerrit");
}
private static <T> Provider<T> find(Injector src, TypeLiteral<T> type) {
List<Binding<T>> 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;
}
}
}

View File

@@ -30,6 +30,22 @@ import java.util.Map;
/** <b>DO NOT USE</b> */
public class PrivateInternals_DynamicTypes {
public static Map<TypeLiteral<?>, DynamicItem<?>> dynamicItemsOf(Injector src) {
Map<TypeLiteral<?>, DynamicItem<?>> m = newHashMap();
for (Map.Entry<Key<?>, 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<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
Map<TypeLiteral<?>, DynamicSet<?>> m = newHashMap();
for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
@@ -62,6 +78,38 @@ public class PrivateInternals_DynamicTypes {
return Collections.unmodifiableMap(m);
}
public static List<RegistrationHandle> attachItems(
Injector src,
Map<TypeLiteral<?>, DynamicItem<?>> items, String pluginName) {
if (src == null || items == null || items.isEmpty()) {
return Collections.emptyList();
}
List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
try {
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();
for (Binding<Object> 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<RegistrationHandle> attachSets(
Injector src,
Map<TypeLiteral<?>, DynamicSet<?>> sets) {

View File

@@ -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);

View File

@@ -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<ModuleGenerator> sshGen;
private Provider<ModuleGenerator> httpGen;
private Map<TypeLiteral<?>, DynamicItem<?>> sysItems;
private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
private Map<TypeLiteral<?>, DynamicSet<?>> httpSets;
@@ -98,6 +103,7 @@ public class PluginGuiceEnvironment {
onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
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<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) {
@@ -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<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) {