|
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