|
36 | 36 | import java.util.ArrayList;
|
37 | 37 | import java.util.Collection;
|
38 | 38 | import java.util.Collections;
|
| 39 | +import java.util.IdentityHashMap; |
39 | 40 | import java.util.Iterator;
|
40 | 41 | import java.util.List;
|
41 | 42 | import java.util.Map;
|
42 | 43 | import java.util.Objects;
|
| 44 | +import java.util.Set; |
43 | 45 | import java.util.Vector;
|
44 | 46 | import java.util.concurrent.ConcurrentHashMap;
|
45 | 47 | import java.util.concurrent.CopyOnWriteArrayList;
|
|
48 | 50 | import jenkins.ExtensionComponentSet;
|
49 | 51 | import jenkins.model.Jenkins;
|
50 | 52 | import jenkins.util.io.OnMaster;
|
| 53 | +import org.kohsuke.accmod.Restricted; |
| 54 | +import org.kohsuke.accmod.restrictions.NoExternalUse; |
51 | 55 |
|
52 | 56 | /**
|
53 | 57 | * Retains the known extension instances for the given type 'T'.
|
@@ -335,27 +339,44 @@ protected Object getLoadLock() {
|
335 | 339 | /**
|
336 | 340 | * Used during {@link Jenkins#refreshExtensions()} to add new components into existing {@link ExtensionList}s.
|
337 | 341 | * Do not call from anywhere else.
|
| 342 | + * @return true if {@link #fireOnChangeListeners} should be called on {@code this} after all lists have been refreshed. |
338 | 343 | */
|
339 |
| - public void refresh(ExtensionComponentSet delta) { |
340 |
| - boolean fireOnChangeListeners = false; |
| 344 | + @Restricted(NoExternalUse.class) |
| 345 | + public boolean refresh(ExtensionComponentSet delta) { |
341 | 346 | synchronized (getLoadLock()) {
|
342 | 347 | if (extensions == null)
|
343 |
| - return; // not yet loaded. when we load it, we'll load everything visible by then, so no work needed |
344 |
| - |
345 |
| - Collection<ExtensionComponent<T>> found = load(delta); |
346 |
| - if (!found.isEmpty()) { |
347 |
| - List<ExtensionComponent<T>> l = new ArrayList<>(extensions); |
348 |
| - l.addAll(found); |
349 |
| - extensions = sort(l); |
350 |
| - fireOnChangeListeners = true; |
| 348 | + return false; // not yet loaded. when we load it, we'll load everything visible by then, so no work needed |
| 349 | + |
| 350 | + Collection<ExtensionComponent<T>> newComponents = load(delta); |
| 351 | + if (!newComponents.isEmpty()) { |
| 352 | + // We check to ensure that we do not insert duplicate instances of already-loaded extensions into the list. |
| 353 | + // This can happen when dynamically loading a plugin with an extension A that itself loads another |
| 354 | + // extension B from the same plugin in some contexts, such as in A's constructor or via a method in A called |
| 355 | + // by an ExtensionListListener. In those cases, ExtensionList.refresh may be called on a list that already |
| 356 | + // includes the new extensions. Note that ExtensionComponent objects are always unique, even when |
| 357 | + // ExtensionComponent.getInstance is identical, so we have to track the components and instances separately |
| 358 | + // to handle ordinal sorting and check for dupes. |
| 359 | + List<ExtensionComponent<T>> components = new ArrayList<>(extensions); |
| 360 | + Set<T> instances = Collections.newSetFromMap(new IdentityHashMap<>()); |
| 361 | + for (ExtensionComponent<T> component : components) { |
| 362 | + instances.add(component.getInstance()); |
| 363 | + } |
| 364 | + boolean fireListeners = false; |
| 365 | + for (ExtensionComponent<T> newComponent : newComponents) { |
| 366 | + if (instances.add(newComponent.getInstance())) { |
| 367 | + fireListeners = true; |
| 368 | + components.add(newComponent); |
| 369 | + } |
| 370 | + } |
| 371 | + extensions = sort(new ArrayList<>(components)); |
| 372 | + return fireListeners; |
351 | 373 | }
|
352 | 374 | }
|
353 |
| - if (fireOnChangeListeners) { |
354 |
| - fireOnChangeListeners(); |
355 |
| - } |
| 375 | + return false; |
356 | 376 | }
|
357 | 377 |
|
358 |
| - private void fireOnChangeListeners() { |
| 378 | + @Restricted(NoExternalUse.class) |
| 379 | + public void fireOnChangeListeners() { |
359 | 380 | for (ExtensionListListener listener : listeners) {
|
360 | 381 | try {
|
361 | 382 | listener.onChange();
|
|
0 commit comments