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
					Brad Larson
				
			
				
					committed by
					
						 Shawn Pearce
						Shawn Pearce
					
				
			
			
				
	
			
			
			 Shawn Pearce
						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