/
GHEventsSubscriber.java
249 lines (231 loc) · 9.94 KB
/
GHEventsSubscriber.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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 overridden 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 overridden 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;
}
};
}
}