com.google.gerrit.extensions.registration.DynamicItem Maven / Gradle / Ivy
// 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.inject.Binder;
import com.google.inject.BindingAnnotation;
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.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
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 {
  /** Annotate a DynamicItem to be final and being bound at most once. */
  @Target({ElementType.TYPE})
  @Retention(RUNTIME)
  @BindingAnnotation
  public @interface Final {
    String implementedByPlugin() default "";
  }
  /**
   * Declare a singleton {@code DynamicItem} with a binder.
   *
   * Items must be defined in a Guice module before they can be bound:
   *
   * 
   *   DynamicItem.itemOf(binder(), Interface.class);
   *   DynamicItem.bind(binder(), Interface.class).to(Impl.class);
   * 
   *
   * @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<Thing<Foo>>() {});
   * 
   *
   * @param binder a new binder created in the module.
   * @param member type of entry to store.
   */
  public static  void itemOf(Binder binder, TypeLiteral member) {
    Key> key = keyFor(member);
    binder.bind(key).toProvider(new DynamicItemProvider<>(member, key)).in(Scopes.SINGLETON);
  }
  /**
   * Construct a single {@code DynamicItem} with a fixed value.
   *
   * Primarily useful for passing {@code DynamicItem}s to constructors in tests.
   *
   * @param member type of item.
   * @param item item to store.
   */
  public static  DynamicItem itemOf(Class member, T item) {
    return new DynamicItem<>(
        keyFor(TypeLiteral.get(member)), Providers.of(item), PluginName.GERRIT);
  }
  @SuppressWarnings("unchecked")
  private static  Key> keyFor(TypeLiteral member) {
    return (Key>)
        Key.get(Types.newParameterizedType(DynamicItem.class, member.getType()));
  }
  /**
   * 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) {
    Extension in = null;
    if (provider != null) {
      in = new Extension<>(pluginName, provider);
    }
    this.key = key;
    this.ref = new AtomicReference<>(in);
  }
  @Nullable
  public Extension getEntry() {
    return ref.get();
  }
  /**
   * 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.
   */
  @Nullable
  public T get() {
    Extension item = ref.get();
    return item != null ? item.get() : null;
  }
  /**
   * Get the name of the plugin that has bound the configured item, or null.
   *
   * @return the name of the plugin that has bound the configured item; null if no implementation
   *     has been bound to the item. This is common if no plugin registered an implementation for
   *     the type.
   */
  @Nullable
  public String getPluginName() {
    Extension item = ref.get();
    return item != null ? item.getPluginName() : 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.
   */
  @CanIgnoreReturnValue
  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.
   */
  @CanIgnoreReturnValue
  public RegistrationHandle set(Provider impl, String pluginName) {
    final Extension item = new Extension<>(pluginName, impl);
    Extension old = null;
    while (!ref.compareAndSet(old, item)) {
      old = ref.get();
      if (old != null && !PluginName.GERRIT.equals(old.getPluginName())) {
        throw new ProvisionException(
            String.format(
                "%s already provided by %s, ignoring plugin %s",
                key.getTypeLiteral(), old.getPluginName(), pluginName));
      }
    }
    final Extension defaultItem = old;
    return () -> ref.compareAndSet(item, defaultItem);
  }
  /**
   * 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.
   */
  @CanIgnoreReturnValue
  public ReloadableRegistrationHandle set(Key key, Provider impl, String pluginName) {
    final Extension item = new Extension<>(pluginName, impl);
    Extension old = null;
    while (!ref.compareAndSet(old, item)) {
      old = ref.get();
      if (old != null
          && !PluginName.GERRIT.equals(old.getPluginName())
          && !pluginName.equals(old.getPluginName())) {
        // We allow to replace:
        // 1. Gerrit core items, e.g. websession cache
        //    can be replaced by plugin implementation
        // 2. Reload of current plugin
        throw new ProvisionException(
            String.format(
                "%s already provided by %s, ignoring plugin %s",
                this.key.getTypeLiteral(), old.getPluginName(), pluginName));
      }
    }
    return new ReloadableHandle(key, item, old);
  }
  private class ReloadableHandle implements ReloadableRegistrationHandle {
    private final Key handleKey;
    private final Extension item;
    private final Extension defaultItem;
    ReloadableHandle(Key handleKey, Extension item, Extension defaultItem) {
      this.handleKey = handleKey;
      this.item = item;
      this.defaultItem = defaultItem;
    }
    @Override
    public Key getKey() {
      return handleKey;
    }
    @Override
    public void remove() {
      ref.compareAndSet(item, defaultItem);
    }
    @Override
    @Nullable
    public ReloadableHandle replace(Key newKey, Provider newItem) {
      Extension n = new Extension<>(item.getPluginName(), newItem);
      if (ref.compareAndSet(item, n)) {
        return new ReloadableHandle(newKey, n, defaultItem);
      }
      return null;
    }
  }
}