/** Copyright 2014 Pieter Rautenbach Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.whatsthatlight.teamcity.hipchat; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import freemarker.template.Template; import freemarker.template.TemplateException; import jetbrains.buildServer.serverSide.Branch; import jetbrains.buildServer.serverSide.BuildServerAdapter; import jetbrains.buildServer.serverSide.BuildStatistics; import jetbrains.buildServer.serverSide.ProjectManager; import jetbrains.buildServer.serverSide.SBuild; import jetbrains.buildServer.serverSide.SBuildServer; import jetbrains.buildServer.serverSide.SFinishedBuild; import jetbrains.buildServer.serverSide.SProject; import jetbrains.buildServer.serverSide.SRunningBuild; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.users.UserSet; import jetbrains.buildServer.vcs.SelectPrevBuildPolicy; public class HipChatServerExtension extends BuildServerAdapter { private static Logger logger = Logger.getLogger("com.whatsthatlight.teamcity.hipchat"); private SBuildServer server; private HipChatConfiguration configuration; private HipChatApiProcessor processor; private static Random rng = new Random(); private String messageFormat; private HashMap<TeamCityEvent, HipChatMessageBundle> eventMap; private HipChatNotificationMessageTemplates templates; private HipChatEmoticonCache emoticonCache; public HipChatServerExtension(@NotNull SBuildServer server, @NotNull HipChatConfiguration configuration, @NotNull HipChatApiProcessor processor, @NotNull HipChatNotificationMessageTemplates templates, @NotNull HipChatEmoticonCache emoticonCache) { this.server = server; //this.configDirectory = serverPaths.getConfigDir(); this.configuration = configuration; this.processor = processor; this.templates = templates; this.messageFormat = HipChatMessageFormat.HTML; this.eventMap = new HashMap<TeamCityEvent, HipChatMessageBundle>(); this.eventMap.put(TeamCityEvent.BUILD_STARTED, new HipChatMessageBundle(HipChatEmoticonSet.POSITIVE, HipChatMessageColour.INFO)); this.eventMap.put(TeamCityEvent.BUILD_SUCCESSFUL, new HipChatMessageBundle(HipChatEmoticonSet.POSITIVE, HipChatMessageColour.SUCCESS)); this.eventMap.put(TeamCityEvent.BUILD_FAILED, new HipChatMessageBundle(HipChatEmoticonSet.NEGATIVE, HipChatMessageColour.ERROR)); this.eventMap.put(TeamCityEvent.BUILD_INTERRUPTED, new HipChatMessageBundle(HipChatEmoticonSet.INDIFFERENT, HipChatMessageColour.WARNING)); this.eventMap.put(TeamCityEvent.SERVER_STARTUP, new HipChatMessageBundle(null, HipChatMessageColour.NEUTRAL)); this.eventMap.put(TeamCityEvent.SERVER_SHUTDOWN,new HipChatMessageBundle(null, HipChatMessageColour.NEUTRAL)); this.emoticonCache = emoticonCache; logger.debug("Server extension created"); } public void register() { this.server.addListener(this); logger.debug("Server extension registered"); //this.controller.IsInitialised(); } @Override public void changesLoaded(SRunningBuild build) { logger.debug(String.format("Build started: %s", build.getBuildType().getName())); super.changesLoaded(build); if (this.configuration.getEvents() != null && this.configuration.getEvents().getBuildStartedStatus()) { this.processBuildEvent(build, TeamCityEvent.BUILD_STARTED); } } @Override public void buildFinished(SRunningBuild build) { super.buildFinished(build); Branch branch = build.getBranch(); List<SFinishedBuild> buildHistory = build.getBuildType().getHistory(); SFinishedBuild previousBuild = null; if (branch != null) { for (SFinishedBuild tmpBuild : buildHistory) { Branch tmpBranch = tmpBuild.getBranch(); if ((build.getBuildId() != tmpBuild.getBuildId()) && tmpBranch.getName().equals(branch.getName())) { previousBuild = tmpBuild; break; } } } else { if (buildHistory.size() > 1) { previousBuild = buildHistory.get(1); } } if (build.getBuildStatus().isSuccessful() && this.configuration.getEvents() != null && this.configuration.getEvents().getBuildSuccessfulStatus()) { if (!this.configuration.getEvents().getOnlyAfterFirstBuildSuccessfulStatus() || previousBuild == null || previousBuild.getBuildStatus().isFailed()) { this.processBuildEvent(build, TeamCityEvent.BUILD_SUCCESSFUL); } } else if (build.getBuildStatus().isFailed() && this.configuration.getEvents() != null && this.configuration.getEvents().getBuildFailedStatus()) { if (!this.configuration.getEvents().getOnlyAfterFirstBuildFailedStatus() || previousBuild == null || previousBuild.getBuildStatus().isSuccessful()) { this.processBuildEvent(build, TeamCityEvent.BUILD_FAILED); } } } @Override public void buildInterrupted(SRunningBuild build) { super.buildInterrupted(build); if (this.configuration.getEvents() != null && this.configuration.getEvents().getBuildInterruptedStatus()) { this.processBuildEvent(build, TeamCityEvent.BUILD_INTERRUPTED); } } @Override public void serverStartup() { if (this.configuration.getEvents() != null && this.configuration.getEvents().getServerStartupStatus()) { this.processServerEvent(TeamCityEvent.SERVER_STARTUP); } } @Override public void serverShutdown() { if (this.configuration.getEvents() != null && this.configuration.getEvents().getServerShutdownStatus()) { this.processServerEvent(TeamCityEvent.SERVER_SHUTDOWN); } } private void processServerEvent(TeamCityEvent event) { try { boolean notify = this.configuration.getDefaultNotifyStatus(); HipChatMessageBundle bundle = this.eventMap.get(event); String colour = bundle.getColour(); String message = renderTemplate(this.templates.readTemplate(event), new HashMap<String, Object>()); HipChatRoomNotification notification = new HipChatRoomNotification(message, this.messageFormat, colour, notify); String roomId = this.configuration.getDefaultRoomId(); if ((event == TeamCityEvent.SERVER_STARTUP || event == TeamCityEvent.SERVER_SHUTDOWN) && this.configuration.getServerEventRoomId() != null) { roomId = this.configuration.getServerEventRoomId(); } if (roomId != null) { this.processor.sendNotification(notification, roomId); } } catch (Exception e) { logger.error(String.format("Error processing server event: %s", event), e); } } private void processBuildEvent(SRunningBuild build, TeamCityEvent event) { try { logger.info(String.format("Received %s build event", event)); if (!this.configuration.getDisabledStatus() && !build.isPersonal()) { Branch branch = build.getBranch(); if ((this.configuration.getBranchFilterEnabledStatus()) && (branch != null)) { String branchDisplayName = branch.getDisplayName(); if (branchDisplayName.matches(this.configuration.getBranchFilterRegex())) { logger.debug(String.format("Branch %s skipped", new Object[] { branchDisplayName })); return; } } logger.info("Processing build event"); String message = createHtmlBuildEventMessage(build, event); String colour = getBuildEventMessageColour(event); ProjectManager projectManager = this.server.getProjectManager(); SProject project = projectManager.findProjectById(build.getProjectId()); HipChatProjectConfiguration projectConfiguration = Utils.determineProjectConfiguration(project, configuration); HipChatRoomNotification notification = new HipChatRoomNotification(message, this.messageFormat, colour, projectConfiguration.getNotifyStatus()); String roomId = projectConfiguration.getRoomId(); logger.debug(String.format("Room to be notified: %s", roomId)); if (!Utils.IsRoomIdNullOrNone(roomId)) { if (roomId.equals(HipChatConfiguration.ROOM_ID_DEFAULT_VALUE)) { roomId = configuration.getDefaultRoomId(); } else if (roomId.equals(HipChatConfiguration.ROOM_ID_PARENT_VALUE)) { HipChatProjectConfiguration parentProjectConfiguration = Utils.findFirstSpecificParentConfiguration(project, configuration); if (parentProjectConfiguration != null) { logger.debug("Using specific configuration in hierarchy determined implicitly"); roomId = parentProjectConfiguration.getRoomId(); notification.notify = parentProjectConfiguration.getNotifyStatus(); } } if (!Utils.IsRoomIdNullOrNone(roomId)) { logger.debug(String.format("Room notified: %s", roomId)); this.processor.sendNotification(notification, roomId); } } } } catch (Exception e) { logger.error("Could not process build event", e); } } private String getBuildEventMessageColour(TeamCityEvent buildEvent) { return this.eventMap.get(buildEvent).getColour(); } private String createHtmlBuildEventMessage(SRunningBuild build, TeamCityEvent buildEvent) throws TemplateException, IOException { HipChatMessageBundle bundle = this.eventMap.get(buildEvent); Template template = this.templates.readTemplate(buildEvent); // Emoticon String emoticon = getRandomEmoticon(bundle.getEmoticonSet()); logger.debug(String.format("Emoticon: %s", emoticon)); String emoticonUrl = this.emoticonCache.get(emoticon); // Branch Branch branch = build.getBranch(); boolean hasBranch = branch != null; logger.debug(String.format("Has branch: %s", hasBranch)); String branchDisplayName = ""; if (hasBranch) { branchDisplayName = branch.getDisplayName(); logger.debug(String.format("Branch: %s", branchDisplayName)); } // Contributors (committers) String contributors = getContributors(build); boolean hasContributors = !contributors.isEmpty(); logger.debug(String.format("Has contributors: %s", hasContributors)); // Fill the template. Map<String, Object> templateMap = new HashMap<String, Object>(); // Build statistics logger.debug("Adding standard build statistics"); BuildStatistics statistics = build.getFullStatistics(); logger.debug(String.format("\tNumber of tests: %s", statistics.getAllTestCount())); logger.debug(String.format("\tNumber of passed tests: %s", statistics.getPassedTestCount())); logger.debug(String.format("\tNumber of failed tests: %s", statistics.getFailedTestCount())); logger.debug(String.format("\tNumber of new failed tests: %s", statistics.getNewFailedCount())); logger.debug(String.format("\tNumber of ignored tests: %s", statistics.getIgnoredTestCount())); logger.debug(String.format("\tTests duration: %s", statistics.getTotalDuration())); templateMap.put(HipChatNotificationMessageTemplates.Parameters.NO_OF_TESTS, statistics.getAllTestCount()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.NO_OF_PASSED_TESTS, statistics.getPassedTestCount()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.NO_OF_FAILED_TESTS, statistics.getFailedTestCount()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.NO_OF_NEW_FAILED_TESTS, statistics.getNewFailedCount()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.NO_OF_IGNORED_TESTS, statistics.getIgnoredTestCount()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.DURATION_OF_TESTS, statistics.getTotalDuration()); logger.debug("Adding discovered build statistics - use in templates by accessing the data model and with prefix stats"); Map<String, BigDecimal> allStatistics = build.getStatisticValues(); for (Map.Entry<String, BigDecimal> statistic : allStatistics.entrySet()) { logger.debug(String.format("\t%s: %s", statistic.getKey(), statistic.getValue())); templateMap.put(String.format("%s.%s", HipChatNotificationMessageTemplates.STATS_PARAMETERS_PREFIX, statistic.getKey()), statistic.getValue()); } // // TODO: Add artifact dependencies as a template variable // try { // SBuildType buildType = build.getBuildType(); // Collection<SBuildType> childDependencies = buildType.getChildDependencies(); // for (SBuildType sBuildType : childDependencies) { // SFinishedBuild changes = sBuildType.getLastChangesFinished(); // changes.getCommitters(SelectPrevBuildPolicy.SINCE_LAST_BUILD); // } // logger.debug(String.format("Children: %s", childDependencies.isEmpty())); // List<Dependency> dependencies = buildType.getDependencies(); // //dependencies.get(0). // logger.debug(String.format("Children: %s", dependencies.isEmpty())); // List<SBuildType> dependencyReferences = buildType.getDependencyReferences(); // logger.debug(String.format("Children: %s", dependencyReferences.isEmpty())); // } catch (Exception e) { // } // Add all available project, build configuration, agent, server, etc. parameters to the data model // These are accessed as ${.data_model["some.variable"]} // See: http://freemarker.org/docs/ref_specvar.html logger.debug("Adding build parameters"); for (Map.Entry<String, String> entry : build.getParametersProvider().getAll().entrySet()) { logger.debug(String.format("\t%s: %s", entry.getKey(), entry.getValue())); templateMap.put(entry.getKey(), entry.getValue()); } logger.debug("Adding agent parameters"); for (Map.Entry<String, String> entry : build.getAgent().getAvailableParameters().entrySet()) { logger.debug(String.format("\t%s: %s", entry.getKey(), entry.getValue())); templateMap.put(entry.getKey(), entry.getValue()); } // Standard plugin parameters logger.debug("Adding standard parameters"); templateMap.put(HipChatNotificationMessageTemplates.Parameters.EMOTICON_URL, emoticonUrl == null ? "" : emoticonUrl); templateMap.put(HipChatNotificationMessageTemplates.Parameters.FULL_NAME, build.getBuildType().getFullName()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.TRIGGERED_BY, build.getTriggeredBy().getAsString()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.HAS_CONTRIBUTORS, hasContributors); templateMap.put(HipChatNotificationMessageTemplates.Parameters.CONTRIBUTORS, contributors); templateMap.put(HipChatNotificationMessageTemplates.Parameters.HAS_BRANCH, hasBranch); templateMap.put(HipChatNotificationMessageTemplates.Parameters.BRANCH, branchDisplayName); templateMap.put(HipChatNotificationMessageTemplates.Parameters.SERVER_URL, this.server.getRootUrl()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.PROJECT_ID, build.getProjectExternalId()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.BUILD_ID, new Long(build.getBuildId()).toString()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.BUILD_TYPE_ID, build.getBuildTypeExternalId()); templateMap.put(HipChatNotificationMessageTemplates.Parameters.BUILD_NUMBER, build.getBuildNumber()); if (buildEvent == TeamCityEvent.BUILD_INTERRUPTED) { long userId = build.getCanceledInfo().getUserId(); SUser user = this.server.getUserModel().findUserById(userId); templateMap.put(HipChatNotificationMessageTemplates.Parameters.CANCELLED_BY, user.getDescriptiveName()); } return renderTemplate(template, templateMap); } private static String getContributors(SBuild build) { UserSet<SUser> committers = build.getCommitters(SelectPrevBuildPolicy.SINCE_LAST_BUILD); Collection<String> userSet = new HashSet<String>(); for (SUser committer : committers.getUsers()) { userSet.add(committer.getDescriptiveName()); } List<String> userList = new ArrayList<String>(userSet); Collections.sort(userList, String.CASE_INSENSITIVE_ORDER); String contributors = Utils.join(userList); return contributors; } private static String renderTemplate(Template template, Map<String, Object> templateMap) throws TemplateException, IOException { Writer writer = new StringWriter(); template.process(templateMap, writer); writer.flush(); String renderedTemplate = writer.toString(); writer.close(); return renderedTemplate; } private static String getRandomEmoticon(String[] set) { int i = rng.nextInt(set.length); return set[i]; } }