package org.jenkinsci.plugins.github.extension;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.Item;
import hudson.model.Job;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.annotation.CheckForNull;
import jenkins.model.Jenkins;
import jenkins.scm.api.SCMEvent;
import org.jenkinsci.plugins.github.util.misc.NullSafeFunction;
import org.jenkinsci.plugins.github.util.misc.NullSafePredicate;
import org.kohsuke.github.GHEvent;
import org.kohsuke.stapler.Stapler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
import static java.util.Collections.emptySet;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
* Extension point to subscribe events from GH, which plugin interested in.
* This point should return true in {@link #isApplicable}
* only if it can parse hooks with events contributed in {@link #events()}
*
* Each time this plugin wants to get events list from subscribers it asks for applicable status
*
* @author lanwen (Merkushev Kirill)
* @since 1.12.0
*/
public abstract class GHEventsSubscriber implements ExtensionPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(GHEventsSubscriber.class);
@CheckForNull
private transient Boolean hasIsApplicableItem;
/**
* Should return true only if this subscriber interested in {@link #events()} set for this project
* Don't call it directly, use {@link #isApplicableFor} static function
*
* @param project to check
*
* @return {@code true} to provide events to register and subscribe for this project
* @deprecated override {@link #isApplicable(Item)} instead.
*/
@Deprecated
protected boolean isApplicable(@Nullable Job<?, ?> project) {
if (checkIsApplicableItem()) {
return isApplicable((Item) project);
}
// a legacy implementation which should not have been calling super.isApplicable(Job)
throw new AbstractMethodError("you must override the new overload of isApplicable");
}
/**
* Should return true only if this subscriber interested in {@link #events()} set for this project
* Don't call it directly, use {@link #isApplicableFor} static function
*
* @param item to check
*
* @return {@code true} to provide events to register and subscribe for this item
* @since 1.25.0
*/
protected abstract boolean isApplicable(@Nullable Item item);
/**
* Call {@link #isApplicable(Item)} with safety for calling to legacy implementations before the abstract method
* was switched from {@link #isApplicable(Job)}.
* @param item to check.
* @return {@code true} to provide events to register and subscribe for this item
*/
@SuppressWarnings("deprecation")
private boolean safeIsApplicable(@Nullable Item item) {
return checkIsApplicableItem() ? isApplicable(item) : item instanceof Job && isApplicable((Job<?, ?>) item);
}
private boolean checkIsApplicableItem() {
if (hasIsApplicableItem == null) {
boolean implemented = false;
// cannot use Util.isOverridden because method is protected and isOverridden only checks public methods
Class<?> clazz = getClass();
while (clazz != null && clazz != GHEventsSubscriber.class) {
try {
Method isApplicable = clazz.getDeclaredMethod("isApplicable", Item.class);
if (isApplicable.getDeclaringClass() != GHEventsSubscriber.class) {
// ok this is the first method we have found that could be an override
// if somebody overrode an inherited method with and `abstract` then we don't have the method
implemented = !Modifier.isAbstract(isApplicable.getModifiers());
break;
}
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
// idempotent so no need for synchronization
this.hasIsApplicableItem = implemented;
}
return hasIsApplicableItem;
}
/**
* Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)}
* Don't call it directly, use {@link #extractEvents()} or {@link #isInterestedIn(GHEvent)} static functions
*
* @return immutable set of events this subscriber wants to register and then subscribe to.
*/
protected abstract Set<GHEvent> events();
/**
* This method called when root action receives webhook from GH and this extension is interested in such
* events (provided by {@link #events()} method). By default do nothing and can be overrided to implement any
* parse logic
* Don't call it directly, use {@link #processEvent(GHSubscriberEvent)} static function
*
* @param event gh-event (as of PUSH, ISSUE...). One of returned by {@link #events()} method. Never null.
* @param payload payload of gh-event. Never blank. Can be parsed with help of GitHub#parseEventPayload
* @deprecated override {@link #onEvent(GHSubscriberEvent)} instead.
*/
@Deprecated
protected void onEvent(GHEvent event, String payload) {
// do nothing by default
}
/**
* This method called when root action receives webhook from GH and this extension is interested in such
* events (provided by {@link #events()} method). By default do nothing and can be overrided to implement any
* parse logic
* Don't call it directly, use {@link #processEvent(GHSubscriberEvent)} static function
*
* @param event the event.
* @since 1.26.0
*/
protected void onEvent(GHSubscriberEvent event) {
onEvent(event.getGHEvent(), event.getPayload());
}
/**
* @return All subscriber extensions
*/
public static ExtensionList<GHEventsSubscriber> all() {
return Jenkins.getInstance().getExtensionList(GHEventsSubscriber.class);
}
/**
* Converts each subscriber to set of GHEvents
*
* @return converter to use in iterable manipulations
*/
public static Function<GHEventsSubscriber, Set<GHEvent>> extractEvents() {
return new NullSafeFunction<GHEventsSubscriber, Set<GHEvent>>() {
@Override
protected Set<GHEvent> applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
return defaultIfNull(subscriber.events(), Collections.<GHEvent>emptySet());
}
};
}
/**
* Helps to filter only GHEventsSubscribers that can return TRUE on given project
*
* @param project to check every GHEventsSubscriber for being applicable
*
* @return predicate to use in iterable filtering
* @see #isApplicable
* @deprecated use {@link #isApplicableFor(Item)}.
*/
@Deprecated
public static Predicate<GHEventsSubscriber> isApplicableFor(final Job<?, ?> project) {
return isApplicableFor((Item) project);
}
/**
* Helps to filter only GHEventsSubscribers that can return TRUE on given item
*
* @param item to check every GHEventsSubscriber for being applicable
*
* @return predicate to use in iterable filtering
* @see #isApplicable
* @since 1.25.0
*/
public static Predicate<GHEventsSubscriber> isApplicableFor(final Item item) {
return new NullSafePredicate<GHEventsSubscriber>() {
@Override
protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
return subscriber.safeIsApplicable(item);
}
};
}
/**
* Predicate which returns true on apply if current subscriber is interested in event
*
* @param event should be one of {@link #events()} set to return true on apply
*
* @return predicate to match against {@link GHEventsSubscriber}
*/
public static Predicate<GHEventsSubscriber> isInterestedIn(final GHEvent event) {
return new NullSafePredicate<GHEventsSubscriber>() {
@Override
protected boolean applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
return defaultIfNull(subscriber.events(), emptySet()).contains(event);
}
};
}
/**
* Function which calls {@link #onEvent(GHSubscriberEvent)} for every subscriber on apply
*
* @param event from hook. Applied only with event from {@link #events()} set
* @param payload string content of hook from GH. Never blank
*
* @return function to process {@link GHEventsSubscriber} list. Returns null on apply.
* @deprecated use {@link #processEvent(GHSubscriberEvent)}
*/
@Deprecated
public static Function<GHEventsSubscriber, Void> processEvent(final GHEvent event, final String payload) {
return processEvent(new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest()), event, payload));
}
/**
* Function which calls {@link #onEvent(GHSubscriberEvent)} for every subscriber on apply
*
* @param event the event
*
* @return function to process {@link GHEventsSubscriber} list. Returns null on apply.
* @since 1.26.0
*/
public static Function<GHEventsSubscriber, Void> processEvent(final GHSubscriberEvent event) {
return new NullSafeFunction<GHEventsSubscriber, Void>() {
@Override
protected Void applyNullSafe(@Nonnull GHEventsSubscriber subscriber) {
try {
subscriber.onEvent(event);
} catch (Throwable t) {
LOGGER.error("Subscriber {} failed to process {} hook, skipping...",
subscriber.getClass().getName(), event, t);
}
return null;
}
};
}
}