package hudson.plugins.tfs;
import hudson.Extension;
import hudson.model.Item;
import hudson.model.Job;
import hudson.model.UnprotectedRootAction;
import hudson.plugins.git.GitStatus;
import hudson.plugins.tfs.model.AbstractHookEvent;
import hudson.plugins.tfs.model.GitPullRequestMergedEvent;
import hudson.plugins.tfs.model.GitPushEvent;
import hudson.plugins.tfs.model.PingHookEvent;
import hudson.plugins.tfs.model.servicehooks.Event;
import hudson.plugins.tfs.util.EndpointHelper;
import hudson.plugins.tfs.util.MediaType;
import hudson.plugins.tfs.util.StringBodyParameter;
import hudson.triggers.Trigger;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_OK;
/**
* The endpoint that TFS/Team Services will POST to on Git code push, pull request merge commit creation, etc.
*/
@Extension
public class TeamEventsEndpoint implements UnprotectedRootAction {
private static final Logger LOGGER = Logger.getLogger(TeamEventsEndpoint.class.getName());
private static final Map<String, AbstractHookEvent.Factory> HOOK_EVENT_FACTORIES_BY_NAME;
static {
final Map<String, AbstractHookEvent.Factory> eventMap =
new TreeMap<String, AbstractHookEvent.Factory>(String.CASE_INSENSITIVE_ORDER);
eventMap.put("ping", new PingHookEvent.Factory());
eventMap.put("gitPullRequestMerged", new GitPullRequestMergedEvent.Factory());
eventMap.put("gitPush", new GitPushEvent.Factory());
HOOK_EVENT_FACTORIES_BY_NAME = Collections.unmodifiableMap(eventMap);
}
public static final String URL_NAME = "team-events";
static final String URL_PREFIX = "/" + URL_NAME + "/";
@Override
public String getIconFileName() {
return null;
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getUrlName() {
return URL_NAME;
}
public HttpResponse doIndex(final HttpServletRequest request) throws IOException {
final Class<? extends TeamEventsEndpoint> me = this.getClass();
final InputStream stream = me.getResourceAsStream("TeamEventsEndpoint.html");
final Jenkins instance = Jenkins.getInstance();
final String rootUrl = instance.getRootUrl();
final String eventRows = describeEvents(HOOK_EVENT_FACTORIES_BY_NAME, URL_NAME);
try {
final String template = IOUtils.toString(stream, MediaType.UTF_8);
final String content = String.format(template, URL_NAME, eventRows, rootUrl);
return HttpResponses.html(content);
}
finally {
IOUtils.closeQuietly(stream);
}
}
static String describeEvents(final Map<String, AbstractHookEvent.Factory> eventMap, final String urlName) {
final String newLine = System.getProperty("line.separator");
final StringBuilder sb = new StringBuilder();
for (final Map.Entry<String, AbstractHookEvent.Factory> eventPair : eventMap.entrySet()) {
final String eventName = eventPair.getKey();
final AbstractHookEvent.Factory factory = eventPair.getValue();
sb.append("<tr>").append(newLine);
sb.append("<td valign='top'>").append(eventName).append("</td>").append(newLine);
sb.append("<td valign='top'>").append('/').append(urlName).append('/').append(eventName).append("</td>").append(newLine);
final String rawSample = factory.getSampleRequestPayload();
final String escapedSample = StringEscapeUtils.escapeHtml4(rawSample);
sb.append("<td><pre>").append(escapedSample).append("</pre></td>").append(newLine);
sb.append("</tr>").append(newLine);
}
return sb.toString();
}
static String pathInfoToEventName(final String pathInfo) {
if (pathInfo.startsWith(URL_PREFIX)) {
final String restOfPath = pathInfo.substring(URL_PREFIX.length());
final int firstSlash = restOfPath.indexOf('/');
final String eventName;
if (firstSlash != -1) {
eventName = restOfPath.substring(0, firstSlash);
}
else {
eventName = restOfPath;
}
return eventName;
}
return null;
}
void dispatch(final StaplerRequest request, final StaplerResponse rsp, final String body) {
final String pathInfo = request.getPathInfo();
final String eventName = pathInfoToEventName(pathInfo);
try {
final JSONObject response = innerDispatch(body, eventName, HOOK_EVENT_FACTORIES_BY_NAME);
rsp.setStatus(SC_OK);
rsp.setContentType(MediaType.APPLICATION_JSON_UTF_8);
final PrintWriter w = rsp.getWriter();
final String responseJsonString = response.toString();
w.print(responseJsonString);
w.println();
}
catch (final IllegalArgumentException e) {
LOGGER.log(Level.WARNING, "IllegalArgumentException", e);
EndpointHelper.error(SC_BAD_REQUEST, e);
}
catch (final Exception e) {
final String template = "Error while performing reaction to '%s' event.";
final String message = String.format(template, eventName);
LOGGER.log(Level.SEVERE, message, e);
EndpointHelper.error(SC_INTERNAL_SERVER_ERROR, e);
}
}
static JSONObject innerDispatch(final String body, final String eventName, final Map<String, AbstractHookEvent.Factory> factoriesByName) throws IOException {
if (StringUtils.isBlank(eventName) || !factoriesByName.containsKey(eventName)) {
throw new IllegalArgumentException("Invalid event");
}
final AbstractHookEvent.Factory factory = factoriesByName.get(eventName);
final Event serviceHookEvent = deserializeEvent(body);
final String message = serviceHookEvent.getMessage().getText();
final String detailedMessage = serviceHookEvent.getDetailedMessage().getText();
final AbstractHookEvent hookEvent = factory.create();
return hookEvent.perform(EndpointHelper.MAPPER, serviceHookEvent, message, detailedMessage);
}
public static Event deserializeEvent(final String input) throws IOException {
final Event serviceHookEvent = EndpointHelper.MAPPER.readValue(input, Event.class);
final String eventType = serviceHookEvent.getEventType();
if (StringUtils.isEmpty(eventType)) {
throw new IllegalArgumentException("Payload did not contain 'eventType'.");
}
// TODO: assert eventType with what Factory claims to support
final Object resource = serviceHookEvent.getResource();
if (resource == null) {
throw new IllegalArgumentException("Payload did not contain 'resource'.");
}
return serviceHookEvent;
}
@RequirePOST
public void doPing(
final StaplerRequest request,
final StaplerResponse response,
@StringBodyParameter @Nonnull final String body) {
dispatch(request, response, body);
}
@RequirePOST
public void doGitPullRequestMerged(
final StaplerRequest request,
final StaplerResponse response,
@StringBodyParameter @Nonnull final String body) {
dispatch(request, response, body);
}
@RequirePOST
public void doGitPush(
final StaplerRequest request,
final StaplerResponse response,
@StringBodyParameter @Nonnull final String body) {
dispatch(request, response, body);
}
public static <T extends Trigger> T findTrigger(final Job<?, ?> job, final Class<T> tClass) {
if (job instanceof ParameterizedJobMixIn.ParameterizedJob) {
final ParameterizedJobMixIn.ParameterizedJob pJob = (ParameterizedJobMixIn.ParameterizedJob) job;
for (final Trigger trigger : pJob.getTriggers().values()) {
if (tClass.isInstance(trigger)) {
return tClass.cast(trigger);
}
}
}
return null;
}
/**
* A response contributor for triggering polling of a project.
*
* @since 1.4.1
*/
public static class PollingScheduledResponseContributor extends GitStatus.ResponseContributor {
/**
* The project
*/
private final Item project;
/**
* Constructor.
*
* @param project the project.
*/
public PollingScheduledResponseContributor(Item project) {
this.project = project;
}
/**
* {@inheritDoc}
*/
@Override
public void addHeaders(StaplerRequest req, StaplerResponse rsp) {
rsp.addHeader("Triggered", project.getAbsoluteUrl());
}
/**
* {@inheritDoc}
*/
@Override
public void writeBody(PrintWriter w) {
w.println("Scheduled polling of " + project.getFullDisplayName());
}
}
public static class ScheduledResponseContributor extends GitStatus.ResponseContributor {
/**
* The project
*/
private final Item project;
/**
* Constructor.
*
* @param project the project.
*/
public ScheduledResponseContributor(Item project) {
this.project = project;
}
/**
* {@inheritDoc}
*/
@Override
public void addHeaders(StaplerRequest req, StaplerResponse rsp) {
rsp.addHeader("Triggered", project.getAbsoluteUrl());
}
/**
* {@inheritDoc}
*/
@Override
public void writeBody(PrintWriter w) {
w.println("Scheduled " + project.getFullDisplayName());
}
}
}