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:
committed by
Shawn Pearce
parent
095a25143c
commit
ae984a98e2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user