/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson; import com.google.common.collect.Lists; import hudson.init.InitMilestone; import hudson.model.Hudson; import jenkins.ExtensionComponentSet; import jenkins.model.Jenkins; import hudson.util.AdaptedIterator; import hudson.util.DescriptorList; import hudson.util.Memoizer; import hudson.util.Iterators; import hudson.ExtensionPoint.LegacyInstancesAreScopedToHudson; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Vector; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; /** * Retains the known extension instances for the given type 'T'. * * <p> * Extensions are loaded lazily on demand and automatically by using {@link ExtensionFinder}, but this * class also provides a mechanism to provide compatibility with the older {@link DescriptorList}-based * manual registration, * * <p> * All {@link ExtensionList} instances should be owned by {@link jenkins.model.Jenkins}, even though * extension points can be defined by anyone on any type. Use {@link jenkins.model.Jenkins#getExtensionList(Class)} * and {@link jenkins.model.Jenkins#getDescriptorList(Class)} to obtain the instances. * * @param <T> * Type of the extension point. This class holds instances of the subtypes of 'T'. * * @author Kohsuke Kawaguchi * @since 1.286 * @see jenkins.model.Jenkins#getExtensionList(Class) * @see jenkins.model.Jenkins#getDescriptorList(Class) */ public class ExtensionList<T> extends AbstractList<T> { /** * @deprecated as of 1.417 * Use {@link #jenkins} */ @Deprecated public final Hudson hudson; public final @CheckForNull Jenkins jenkins; public final Class<T> extensionType; /** * Once discovered, extensions are retained here. */ @CopyOnWrite private volatile List<ExtensionComponent<T>> extensions; private final List<ExtensionListListener> listeners = new CopyOnWriteArrayList<ExtensionListListener>(); /** * Place to store manually registered instances with the per-Hudson scope. * {@link CopyOnWriteArrayList} is used here to support concurrent iterations and mutation. */ private final CopyOnWriteArrayList<ExtensionComponent<T>> legacyInstances; /** * @deprecated as of 1.416 * Use {@link #ExtensionList(Jenkins, Class)} */ @Deprecated protected ExtensionList(Hudson hudson, Class<T> extensionType) { this((Jenkins)hudson,extensionType); } protected ExtensionList(Jenkins jenkins, Class<T> extensionType) { this(jenkins,extensionType,new CopyOnWriteArrayList<ExtensionComponent<T>>()); } /** * @deprecated as of 1.416 * Use {@link #ExtensionList(Jenkins, Class, CopyOnWriteArrayList)} */ @Deprecated protected ExtensionList(Hudson hudson, Class<T> extensionType, CopyOnWriteArrayList<ExtensionComponent<T>> legacyStore) { this((Jenkins)hudson,extensionType,legacyStore); } /** * * @param legacyStore * Place to store manually registered instances. The version of the constructor that * omits this uses a new {@link Vector}, making the storage lifespan tied to the life of {@link ExtensionList}. * If the manually registered instances are scoped to VM level, the caller should pass in a static list. */ protected ExtensionList(Jenkins jenkins, Class<T> extensionType, CopyOnWriteArrayList<ExtensionComponent<T>> legacyStore) { this.hudson = (Hudson)jenkins; this.jenkins = jenkins; this.extensionType = extensionType; this.legacyInstances = legacyStore; if (jenkins == null) { extensions = Collections.emptyList(); } } /** * Add a listener to the extension list. * @param listener The listener. */ public void addListener(@Nonnull ExtensionListListener listener) { listeners.add(listener); } /** * Looks for the extension instance of the given type (subclasses excluded), * or return null. */ public @CheckForNull <U extends T> U get(Class<U> type) { for (T ext : this) if(ext.getClass()==type) return type.cast(ext); return null; } @Override public Iterator<T> iterator() { // we need to intercept mutation, so for now don't allow Iterator.remove return new AdaptedIterator<ExtensionComponent<T>,T>(Iterators.readOnly(ensureLoaded().iterator())) { protected T adapt(ExtensionComponent<T> item) { return item.getInstance(); } }; } /** * Gets the same thing as the 'this' list represents, except as {@link ExtensionComponent}s. */ public List<ExtensionComponent<T>> getComponents() { return Collections.unmodifiableList(ensureLoaded()); } public T get(int index) { return ensureLoaded().get(index).getInstance(); } public int size() { return ensureLoaded().size(); } /** * Gets the read-only view of this {@link ExtensionList} where components are reversed. */ public List<T> reverseView() { return new AbstractList<T>() { @Override public T get(int index) { return ExtensionList.this.get(size()-index-1); } @Override public int size() { return ExtensionList.this.size(); } }; } @Override public boolean remove(Object o) { try { return removeSync(o); } finally { if(extensions!=null) { fireOnChangeListeners(); } } } private synchronized boolean removeSync(Object o) { boolean removed = removeComponent(legacyInstances, o); if(extensions!=null) { List<ExtensionComponent<T>> r = new ArrayList<ExtensionComponent<T>>(extensions); removed |= removeComponent(r,o); extensions = sort(r); } return removed; } private <T> boolean removeComponent(Collection<ExtensionComponent<T>> collection, Object t) { for (Iterator<ExtensionComponent<T>> itr = collection.iterator(); itr.hasNext();) { ExtensionComponent<T> c = itr.next(); if (c.getInstance().equals(t)) { return collection.remove(c); } } return false; } @Override public final synchronized T remove(int index) { T t = get(index); remove(t); return t; } /** * Write access will put the instance into a legacy store. * * @deprecated since 2009-02-23. * Prefer automatic registration. */ @Override @Deprecated public boolean add(T t) { try { return addSync(t); } finally { if(extensions!=null) { fireOnChangeListeners(); } } } private synchronized boolean addSync(T t) { legacyInstances.add(new ExtensionComponent<T>(t)); // if we've already filled extensions, add it if(extensions!=null) { List<ExtensionComponent<T>> r = new ArrayList<ExtensionComponent<T>>(extensions); r.add(new ExtensionComponent<T>(t)); extensions = sort(r); } return true; } @Override public void add(int index, T element) { add(element); } /** * Used to bind extension to URLs by their class names. * * @since 1.349 */ public T getDynamic(String className) { for (T t : this) if (t.getClass().getName().equals(className)) return t; return null; } private List<ExtensionComponent<T>> ensureLoaded() { if(extensions!=null) return extensions; // already loaded if (jenkins.getInitLevel().compareTo(InitMilestone.PLUGINS_PREPARED)<0) return legacyInstances; // can't perform the auto discovery until all plugins are loaded, so just make the legacy instances visible synchronized (getLoadLock()) { if(extensions==null) { List<ExtensionComponent<T>> r = load(); r.addAll(legacyInstances); extensions = sort(r); } return extensions; } } /** * Chooses the object that locks the loading of the extension instances. */ protected Object getLoadLock() { return jenkins.lookup.setIfNull(Lock.class,new Lock()); } /** * Used during {@link Jenkins#refreshExtensions()} to add new components into existing {@link ExtensionList}s. * Do not call from anywhere else. */ public void refresh(ExtensionComponentSet delta) { boolean fireOnChangeListeners = false; synchronized (getLoadLock()) { if (extensions==null) return; // not yet loaded. when we load it, we'll load everything visible by then, so no work needed Collection<ExtensionComponent<T>> found = load(delta); if (!found.isEmpty()) { List<ExtensionComponent<T>> l = Lists.newArrayList(extensions); l.addAll(found); extensions = sort(l); fireOnChangeListeners = true; } } if (fireOnChangeListeners) { fireOnChangeListeners(); } } private void fireOnChangeListeners() { for (ExtensionListListener listener : listeners) { try { listener.onChange(); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Error firing ExtensionListListener.onChange().", e); } } } /** * Loading an {@link ExtensionList} can result in a nested loading of another {@link ExtensionList}. * What that means is that we need a single lock that spans across all the {@link ExtensionList}s, * or else we can end up in a dead lock. */ private static final class Lock {} /** * Loads all the extensions. */ protected List<ExtensionComponent<T>> load() { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE,"Loading ExtensionList: "+extensionType, new Throwable()); return jenkins.getPluginManager().getPluginStrategy().findComponents(extensionType, hudson); } /** * Picks up extensions that we care from the given list. */ protected Collection<ExtensionComponent<T>> load(ExtensionComponentSet delta) { return delta.find(extensionType); } /** * If the {@link ExtensionList} implementation requires sorting extensions, * override this method to do so. * * <p> * The implementation should copy a list, do a sort, and return the new instance. */ protected List<ExtensionComponent<T>> sort(List<ExtensionComponent<T>> r) { r = new ArrayList<ExtensionComponent<T>>(r); Collections.sort(r); return r; } /** * @deprecated as of 1.416 * Use {@link #create(Jenkins, Class)} */ @Deprecated public static <T> ExtensionList<T> create(Hudson hudson, Class<T> type) { return create((Jenkins)hudson,type); } public static <T> ExtensionList<T> create(Jenkins jenkins, Class<T> type) { if(type.getAnnotation(LegacyInstancesAreScopedToHudson.class)!=null) return new ExtensionList<T>(jenkins,type); else { return new ExtensionList<T>(jenkins,type,staticLegacyInstances.get(type)); } } /** * Gets the extension list for a given type. * Normally calls {@link Jenkins#getExtensionList(Class)} but falls back to an empty list * in case {@link Jenkins#getInstanceOrNull()} is null. * Thus it is useful to call from {@code all()} methods which need to behave gracefully during startup or shutdown. * @param type the extension point type * @return some list * @since 1.572 */ public static @Nonnull <T> ExtensionList<T> lookup(Class<T> type) { Jenkins j = Jenkins.getInstanceOrNull(); return j == null ? create((Jenkins) null, type) : j.getExtensionList(type); } /** * Places to store static-scope legacy instances. */ private static final Memoizer<Class,CopyOnWriteArrayList> staticLegacyInstances = new Memoizer<Class,CopyOnWriteArrayList>() { public CopyOnWriteArrayList compute(Class key) { return new CopyOnWriteArrayList(); } }; /** * Exposed for the test harness to clear all legacy extension instances. */ public static void clearLegacyInstances() { staticLegacyInstances.clear(); } private static final Logger LOGGER = Logger.getLogger(ExtensionList.class.getName()); }