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) {