package hudson.model; import hudson.ExtensionList; import hudson.ExtensionPoint; import hudson.scm.SCMDescriptor; import java.util.ArrayList; import java.util.List; import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import jenkins.ExtensionFilter; import jenkins.util.SystemProperties; /** * Hides {@link Descriptor}s from users. * * @author Kohsuke Kawaguchi * @since 1.393 * @see ExtensionFilter */ public abstract class DescriptorVisibilityFilter implements ExtensionPoint { private static final Logger LOGGER = Logger.getLogger(DescriptorVisibilityFilter.class.getName()); /** * Decides if the given descriptor should be visible to the user. * * @param contextClass The class of object that indicates where the visibility of a descriptor is evaluated. * For example, if Jenkins is deciding whether a {@link FreeStyleProject} should gets a * {@link SCMDescriptor}, the context class will be {@link FreeStyleProject}. * @param descriptor Descriptor whose visibility is evaluated. Never null. * @return true to allow the descriptor to be visible. false to hide it. * If any of the installed {@link DescriptorVisibilityFilter} returns false, * the descriptor is not shown. * @since 2.12 */ public boolean filterType(@Nonnull Class<?> contextClass, @Nonnull Descriptor descriptor) { return true; } /** * Decides if the given descriptor should be visible to the user. * * @param context * The object that indicates where the visibility of a descriptor is evaluated. * For example, if Hudson is deciding whether a {@link FreeStyleProject} should gets a * {@link SCMDescriptor}, the context object will be the {@link FreeStyleProject}. * The caller can pass in null if there's no context. * @param descriptor * Descriptor whose visibility is evaluated. Never null. * * @return * true to allow the descriptor to be visible. false to hide it. * If any of the installed {@link DescriptorVisibilityFilter} returns false, * the descriptor is not shown. */ public abstract boolean filter(@CheckForNull Object context, @Nonnull Descriptor descriptor); public static ExtensionList<DescriptorVisibilityFilter> all() { return ExtensionList.lookup(DescriptorVisibilityFilter.class); } public static <T extends Descriptor> List<T> apply(Object context, Iterable<T> source) { ExtensionList<DescriptorVisibilityFilter> filters = all(); List<T> r = new ArrayList<T>(); Class<?> contextClass = context == null ? null : context.getClass(); OUTER: for (T d : source) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Determining visibility of " + d + " in context " + context); } for (DescriptorVisibilityFilter f : filters) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Querying " + f + " for visibility of " + d + " in " + context); } try { if (contextClass != null && !f.filterType(contextClass, d)) { if (LOGGER.isLoggable(Level.CONFIG)) { LOGGER.config("Filter " + f + " hides " + d + " in contexts of type " + contextClass); } continue OUTER; // veto-ed. not shown } if (!f.filter(context, d)) { if (LOGGER.isLoggable(Level.CONFIG)) { LOGGER.config("Filter " + f + " hides " + d + " in context " + context); } continue OUTER; // veto-ed. not shown } } catch (Error e) { LOGGER.log(Level.WARNING, "Encountered error while processing filter " + f + " for context " + context, e); throw e; } catch (Throwable e) { LOGGER.log(logLevelFor(f), "Uncaught exception from filter " + f + " for context " + context, e); continue OUTER; // veto-ed. not shown } } r.add(d); } return r; } public static <T extends Descriptor> List<T> applyType(Class<?> contextClass, Iterable<T> source) { ExtensionList<DescriptorVisibilityFilter> filters = all(); List<T> r = new ArrayList<T>(); OUTER: for (T d : source) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Determining visibility of " + d + " in contexts of type " + contextClass); } for (DescriptorVisibilityFilter f : filters) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Querying " + f + " for visibility of " + d + " in type " + contextClass); } try { if (contextClass != null && !f.filterType(contextClass, d)) { if (LOGGER.isLoggable(Level.CONFIG)) { LOGGER.config("Filter " + f + " hides " + d + " in contexts of type " + contextClass); } continue OUTER; // veto-ed. not shown } } catch (Error e) { LOGGER.log(Level.WARNING, "Encountered error while processing filter " + f + " for contexts of type " + contextClass, e); throw e; } catch (Throwable e) { LOGGER.log(logLevelFor(f), "Uncaught exception from filter " + f + " for context of type " + contextClass, e); continue OUTER; // veto-ed. not shown } } r.add(d); } return r; } /** * Returns the {@link Level} to log an uncaught exception from a {@link DescriptorVisibilityFilter}. We * need to suppress repeated exceptions as there can be many invocations of the {@link DescriptorVisibilityFilter} * triggered by the UI and spamming the logs would be bad. * * @param f the {@link DescriptorVisibilityFilter}. * @return the level to report uncaught exceptions at. */ private static Level logLevelFor(DescriptorVisibilityFilter f) { Long interval = SystemProperties.getLong( DescriptorVisibilityFilter.class.getName() + ".badFilterLogWarningIntervalMinutes", 60L); // the healthy path will never see this synchronized block synchronized (ResourceHolder.BAD_FILTERS) { Long lastTime = ResourceHolder.BAD_FILTERS.get(f); if (lastTime == null || lastTime + TimeUnit.MINUTES.toMillis(interval) < System.currentTimeMillis()) { ResourceHolder.BAD_FILTERS.put(f, System.currentTimeMillis()); return Level.WARNING; } else { return Level.FINE; } } } /** * Lazy initialization singleton for the map of bad filters. Should never be instantiated in a healthy instance. */ private static final class ResourceHolder { /** * The last time we complained in the logs about specific filters. */ private static final WeakHashMap<DescriptorVisibilityFilter, Long> BAD_FILTERS = new WeakHashMap<>(); } }