summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/extensions/registration/DynamicItem.java
blob: 67982d9040eec59ae168235f0c9e1a176231bb98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// 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.gerrit.common.Nullable;
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> {
  /**
   * 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&lt;Thing&lt;Foo&gt;&gt;() {});
   * </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) {
    Key<DynamicItem<T>> key = keyFor(member);
    binder.bind(key).toProvider(new DynamicItemProvider<>(member, key)).in(Scopes.SINGLETON);
  }

  /**
   * Construct a single {@code DynamicItem<T>} with a fixed value.
   *
   * <p>Primarily useful for passing {@code DynamicItem}s to constructors in tests.
   *
   * @param member type of item.
   * @param item item to store.
   */
  public static <T> DynamicItem<T> itemOf(Class<T> member, T item) {
    return new DynamicItem<>(
        keyFor(TypeLiteral.get(member)), Providers.of(item), PluginName.GERRIT);
  }

  @SuppressWarnings("unchecked")
  private static <T> Key<DynamicItem<T>> keyFor(TypeLiteral<T> member) {
    return (Key<DynamicItem<T>>)
        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 <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<Extension<T>> ref;

  DynamicItem(Key<DynamicItem<T>> key, Provider<T> provider, String pluginName) {
    Extension<T> in = null;
    if (provider != null) {
      in = new Extension<>(pluginName, provider);
    }
    this.key = key;
    this.ref = new AtomicReference<>(in);
  }

  @Nullable
  public Extension<T> 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<T> 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<T> 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.
   */
  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 Extension<T> item = new Extension<>(pluginName, impl);
    Extension<T> 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<T> defaultItem = old;
    return new RegistrationHandle() {
      @Override
      public void remove() {
        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.
   */
  public ReloadableRegistrationHandle<T> set(Key<T> key, Provider<T> impl, String pluginName) {
    final Extension<T> item = new Extension<>(pluginName, impl);
    Extension<T> 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<T> {
    private final Key<T> handleKey;
    private final Extension<T> item;
    private final Extension<T> defaultItem;

    ReloadableHandle(Key<T> handleKey, Extension<T> item, Extension<T> defaultItem) {
      this.handleKey = handleKey;
      this.item = item;
      this.defaultItem = defaultItem;
    }

    @Override
    public Key<T> getKey() {
      return handleKey;
    }

    @Override
    public void remove() {
      ref.compareAndSet(item, defaultItem);
    }

    @Override
    @Nullable
    public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
      Extension<T> n = new Extension<>(item.getPluginName(), newItem);
      if (ref.compareAndSet(item, n)) {
        return new ReloadableHandle(newKey, n, defaultItem);
      }
      return null;
    }
  }
}