package hudson.plugins.im;
import static hudson.plugins.im.tools.BuildHelper.getProjectName;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.User;
import hudson.model.UserProperty;
import hudson.model.Fingerprint.RangeSet;
import hudson.plugins.im.tools.Assert;
import hudson.plugins.im.tools.BuildHelper;
import hudson.plugins.im.tools.ExceptionHelper;
import hudson.plugins.im.tools.MessageHelper;
import hudson.scm.ChangeLogSet;
import hudson.scm.ChangeLogSet.Entry;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Notifier;
import hudson.tasks.Publisher;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
/**
* The actual Publisher that sends notification-Messages out to the clients.
*
* @author Uwe Schaefer
* @author Christoph Kutzinski
*/
public abstract class IMPublisher extends Notifier implements BuildStep
{
private static final Logger LOGGER = Logger.getLogger(IMPublisher.class.getName());
private List<IMMessageTarget> targets;
/**
* @deprecated only left here to deserialize old configs
*/
@Deprecated
private hudson.plugins.jabber.NotificationStrategy notificationStrategy;
private NotificationStrategy strategy;
private final boolean notifyOnBuildStart;
private final boolean notifySuspects;
private final boolean notifyCulprits;
private final boolean notifyFixers;
private final boolean notifyUpstreamCommitters;
/**
* @deprecated Only for deserializing old instances
*/
@SuppressWarnings("unused")
@Deprecated
private transient String defaultIdSuffix;
protected IMPublisher(List<IMMessageTarget> defaultTargets,
String notificationStrategyString,
boolean notifyGroupChatsOnBuildStart,
boolean notifySuspects,
boolean notifyCulprits,
boolean notifyFixers,
boolean notifyUpstreamCommitters)
{
if (defaultTargets != null) {
this.targets = defaultTargets;
} else {
this.targets = Collections.emptyList();
}
NotificationStrategy strategy = NotificationStrategy.forDisplayName(notificationStrategyString);
if (strategy == null) {
strategy = NotificationStrategy.STATECHANGE_ONLY;
}
this.strategy = strategy;
this.notifyOnBuildStart = notifyGroupChatsOnBuildStart;
this.notifySuspects = notifySuspects;
this.notifyCulprits = notifyCulprits;
this.notifyFixers = notifyFixers;
this.notifyUpstreamCommitters = notifyUpstreamCommitters;
}
/**
* {@inheritDoc}
*/
@Override
public boolean needsToRunAfterFinalized() {
// notifyUpstreamCommitters needs the fingerprints to be generated
// which seems to happen quite late in the build
return this.notifyUpstreamCommitters;
}
/**
* Returns a short name of the plugin to be used e.g. in log messages.
*/
protected abstract String getPluginName();
protected abstract IMConnection getIMConnection() throws IMException;
protected NotificationStrategy getNotificationStrategy() {
return strategy;
}
protected void setNotificationStrategy(NotificationStrategy strategy) {
this.strategy = strategy;
}
/**
* Returns the notification targets configured on a per-job basis.
*/
public List<IMMessageTarget> getNotificationTargets() {
return this.targets;
}
/**
* Returns the notification target which should actually be used for notification.
*
* Differs from {@link #getNotificationTargets()} because it also takes
* {@link IMPublisherDescriptor#getDefaultTargets()} into account!
*/
protected List<IMMessageTarget> calculateTargets() {
if (getNotificationTargets() != null && getNotificationTargets().size() > 0) {
return getNotificationTargets();
}
return ((IMPublisherDescriptor)getDescriptor()).getDefaultTargets();
}
/**
* Returns the notification targets as a string suitable for
* display in the settings page.
*
* Returns an empty string if no targets are set.
*/
public String getTargets() {
if (this.targets == null) {
return "";
}
final StringBuilder sb = new StringBuilder();
for (final IMMessageTarget t : this.targets) {
sb.append(getIMDescriptor().getIMMessageTargetConverter().toString(t));
sb.append(" ");
}
return sb.toString().trim();
}
@Deprecated
protected void setTargets(String targetsAsString) throws IMMessageTargetConversionException {
this.targets = new LinkedList<IMMessageTarget>();
final String[] split = targetsAsString.split("\\s");
final IMMessageTargetConverter conv = getIMDescriptor().getIMMessageTargetConverter();
for (final String fragment : split)
{
IMMessageTarget createIMMessageTarget;
createIMMessageTarget = conv.fromString(fragment);
if (createIMMessageTarget != null)
{
this.targets.add(createIMMessageTarget);
}
}
}
/**
* @deprecated Should only be used to deserialize old instances
*/
@Deprecated
protected void setNotificationTargets(List<IMMessageTarget> targets) {
if (targets != null) {
this.targets = targets;
} else {
this.targets = Collections.emptyList();
}
}
/**
* Returns the selected notification strategy as a string
* suitable for display.
*/
public final String getStrategy() {
return getNotificationStrategy().getDisplayName();
}
/**
* Specifies if the starting of builds should be notified to
* the registered chat rooms.
*/
public final boolean getNotifyOnStart() {
return notifyOnBuildStart;
}
/**
* Specifies if committers to failed builds should be informed about
* build failures.
*/
public final boolean getNotifySuspects() {
return notifySuspects;
}
/**
* Specifies if culprits - i.e. committers to previous already failing
* builds - should be informed about subsequent build failures.
*/
public final boolean getNotifyCulprits() {
return notifyCulprits;
}
/**
* Specifies if 'fixers' should be informed about
* fixed builds.
*/
public final boolean getNotifyFixers() {
return notifyFixers;
}
/**
* Specifies if upstream committers should be informed about
* build failures.
*/
public final boolean getNotifyUpstreamCommitters() {
return notifyUpstreamCommitters;
}
/**
* Logs message to the build listener's logger.
*/
protected void log(BuildListener listener, String message) {
listener.getLogger().append(getPluginName()).append(": ").append(message).append("\n");
}
@Override
public boolean perform(final AbstractBuild<?,?> build, final Launcher launcher, final BuildListener buildListener)
throws InterruptedException
{
Assert.isNotNull(build, "Parameter 'build' must not be null.");
Assert.isNotNull(buildListener, "Parameter 'buildListener' must not be null.");
if (getNotificationStrategy().notificationWanted(build)) {
notifyChats(build, buildListener);
}
if (BuildHelper.isStillFailureOrUnstable(build)) {
if (this.notifySuspects) {
log(buildListener, "Notifying suspects");
final String message = "Build " + getProjectName(build) +
" is " + BuildHelper.getResultDescription(build) + ": " + MessageHelper.getBuildURL(build);
for (IMMessageTarget target : calculateIMTargets(getCommitters(build), buildListener)) {
try {
log(buildListener, "Sending notification to suspect: " + target.toString());
sendNotification(message, target, buildListener);
} catch (final Throwable e) {
log(buildListener, "There was an error sending suspect notification to: " + target.toString());
}
}
}
if (this.notifyCulprits) {
log(buildListener, "Notifying culprits");
final String message = "You're still being suspected of having broken " + getProjectName(build) + ": " + MessageHelper.getBuildURL(build);
for (IMMessageTarget target : calculateIMTargets(getCulpritsOnly(build), buildListener)) {
try {
log(buildListener, "Sending notification to culprit: " + target.toString());
sendNotification(message, target, buildListener);
} catch (final Throwable e) {
log(buildListener, "There was an error sending culprit notification to: " + target.toString());
}
}
}
} else if (BuildHelper.isFailureOrUnstable(build)) {
boolean committerNotified = false;
if (this.notifySuspects) {
log(buildListener, "Notifying suspects");
String message = "Oh no! You're suspected of having broken " + getProjectName(build) + ": " + MessageHelper.getBuildURL(build);
for (IMMessageTarget target : calculateIMTargets(getCommitters(build), buildListener)) {
try {
log(buildListener, "Sending notification to suspect: " + target.toString());
sendNotification(message, target, buildListener);
committerNotified = true;
} catch (final Throwable e) {
log(buildListener, "There was an error sending suspect notification to: " + target.toString());
}
}
}
if (this.notifyUpstreamCommitters && !committerNotified) {
notifyUpstreamCommitters(build, buildListener);
}
}
if (this.notifyFixers && BuildHelper.isFix(build)) {
buildListener.getLogger().append("Notifying fixers\n");
final String message = "Yippie! Seems you've fixed " + getProjectName(build) + ": " + MessageHelper.getBuildURL(build);
for (IMMessageTarget target : calculateIMTargets(getCommitters(build), buildListener)) {
try {
log(buildListener, "Sending notification to fixer: " + target.toString());
sendNotification(message, target, buildListener);
} catch (final Throwable e) {
log(buildListener, "There was an error sending fixer notification to: " + target.toString());
}
}
}
return true;
}
private void sendNotification(String message, IMMessageTarget target, BuildListener buildListener)
throws IMException {
IMConnection imConnection = getIMConnection();
if (imConnection instanceof DummyConnection) {
// quite hacky
log(buildListener, "[ERROR] not connected. Cannot send message to '" + target + "'");
} else {
getIMConnection().send(target, message);
}
}
/**
* Looks for committers in the direct upstream builds and notifies them.
* If no committers are found in the next higher level, look one level higher.
* Repeat if necessary.
*/
@SuppressWarnings("unchecked")
private void notifyUpstreamCommitters(final AbstractBuild<?, ?> build,
final BuildListener buildListener) {
boolean committerNotified = false;
Map<AbstractProject, Integer> upstreamBuilds = build.getUpstreamBuilds();
while (!committerNotified && !upstreamBuilds.isEmpty()) {
Map<AbstractProject, Integer> currentLevel = upstreamBuilds;
// new map for the builds one level higher up:
upstreamBuilds = new HashMap<AbstractProject, Integer>();
for (Map.Entry<AbstractProject, Integer> entry : currentLevel.entrySet()) {
AbstractBuild<?, ?> upstreamBuild = (AbstractBuild<?, ?>) entry.getKey().getBuildByNumber(entry.getValue());
if (upstreamBuild != null) {
if (! downstreamIsFirstInRangeTriggeredByUpstream(upstreamBuild, build)) {
continue;
}
Set<User> committers = getCommitters(upstreamBuild);
String message = "Attention! Your change in " + getProjectName(upstreamBuild)
+ ": " + MessageHelper.getBuildURL(upstreamBuild)
+ " *might* have broken the downstream job " + getProjectName(build) + ": " + MessageHelper.getBuildURL(build)
+ "\nPlease have a look!";
for (IMMessageTarget target : calculateIMTargets(committers, buildListener)) {
try {
log(buildListener, "Sending notification to upstream committer: " + target.toString());
sendNotification(message, target, buildListener);
committerNotified = true;
} catch (final Throwable e) {
log(buildListener, "There was an error sending upstream committer notification to: " + target.toString());
}
}
}
if (!committerNotified) {
upstreamBuilds.putAll(upstreamBuild.getUpstreamBuilds());
}
}
}
}
/**
* Determines if downstreamBuild is the 1st build of the downstream project
* which has a dependency to the upstreamBuild.
*/
//@Bug(6712)
private boolean downstreamIsFirstInRangeTriggeredByUpstream(
AbstractBuild<?, ?> upstreamBuild, AbstractBuild<?, ?> downstreamBuild) {
RangeSet rangeSet = upstreamBuild.getDownstreamRelationship(downstreamBuild.getProject());
if (rangeSet.isEmpty()) {
// should not happen
LOGGER.warning("Range set is empty. Upstream " + upstreamBuild + ", downstream " + downstreamBuild);
return false;
}
if (rangeSet.min() == downstreamBuild.getNumber()) {
return true;
}
return false;
}
/**
* Notify all registered chats about the build result.
*/
private void notifyChats(final AbstractBuild<?, ?> build, final BuildListener buildListener) {
final StringBuilder sb;
if (BuildHelper.isFix(build)) {
sb = new StringBuilder("Yippie, build fixed!\n");
} else {
sb = new StringBuilder();
}
sb.append("Project ").append(getProjectName(build))
.append(" build (").append(build.getNumber()).append("): ")
.append(BuildHelper.getResultDescription(build)).append(" in ")
.append(build.getTimestampString())
.append(": ")
.append(MessageHelper.getBuildURL(build));
if (! build.getChangeSet().isEmptySet()) {
boolean hasManyChangeSets = build.getChangeSet().getItems().length > 1;
for (Entry entry : build.getChangeSet()) {
sb.append("\n");
if (hasManyChangeSets) {
sb.append("* ");
}
sb.append(entry.getAuthor()).append(": ").append(entry.getMsg());
}
}
final String msg = sb.toString();
for (IMMessageTarget target : calculateTargets())
{
try {
log(buildListener, "Sending notification to: " + target.toString());
sendNotification(msg, target, buildListener);
} catch (final Throwable t) {
log(buildListener, "There was an error sending notification to: " + target.toString() + "\n" + ExceptionHelper.dump(t));
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean prebuild(AbstractBuild<?, ?> build, BuildListener buildListener) {
try {
if (notifyOnBuildStart) {
final StringBuilder sb = new StringBuilder("Starting build ").append(build.getNumber())
.append(" for job ").append(getProjectName(build));
if (build.getPreviousBuild() != null) {
sb.append(" (previous build: ")
.append(BuildHelper.getResultDescription(build.getPreviousBuild()));
if (build.getPreviousBuild().getResult().isWorseThan(Result.SUCCESS)) {
AbstractBuild<?, ?> lastSuccessfulBuild = build.getPreviousNotFailedBuild();
if (lastSuccessfulBuild != null) {
sb.append(" -- last ").append(BuildHelper.getResultDescription(lastSuccessfulBuild))
.append(" #").append(lastSuccessfulBuild.getNumber())
.append(" ").append(lastSuccessfulBuild.getTimestampString()).append(" ago");
}
}
sb.append(")");
}
final String msg = sb.toString();
for (final IMMessageTarget target : calculateTargets()) {
// only notify group chats
if (target instanceof GroupChatIMMessageTarget) {
try {
sendNotification(msg, target, buildListener);
} catch (final Throwable e) {
log(buildListener, "There was an error sending notification to: " + target.toString());
}
}
}
}
} catch (Throwable t) {
// ignore: never, ever cancel a build because a notification fails
log(buildListener, "There was an error in the IM plugin: " + ExceptionHelper.dump(t));
}
return true;
}
private static Set<User> getCommitters(AbstractBuild<?, ?> build) {
Set<User> committers = new HashSet<User>();
ChangeLogSet<? extends Entry> changeSet = build.getChangeSet();
for (Entry entry : changeSet) {
committers.add(entry.getAuthor());
}
return committers;
}
/**
* Returns the culprits WITHOUT the committers to the current build.
*/
private static Set<User> getCulpritsOnly(AbstractBuild<?, ?> build) {
Set<User> culprits = new HashSet<User>(build.getCulprits());
culprits.removeAll(getCommitters(build));
return culprits;
}
private Collection<IMMessageTarget> calculateIMTargets(Set<User> targets, BuildListener listener) {
Set<IMMessageTarget> suspects = new HashSet<IMMessageTarget>();
String defaultIdSuffix = ((IMPublisherDescriptor)getDescriptor()).getDefaultIdSuffix();
LOGGER.fine("Default Suffix: " + defaultIdSuffix);
for (User target : targets) {
LOGGER.fine("Possible target: " + target.getId());
String imId = getConfiguredIMId(target);
if (imId == null && defaultIdSuffix != null) {
imId = target.getId() + defaultIdSuffix;
}
if (imId != null) {
try {
suspects.add(getIMDescriptor().getIMMessageTargetConverter().fromString(imId));
} catch (final IMMessageTargetConversionException e) {
log(listener, "Invalid IM ID: " + imId);
}
} else {
log(listener, "No IM ID found for: " + target.getId());
}
}
return suspects;
}
/**
* {@inheritDoc}
*/
@Override
public abstract BuildStepDescriptor<Publisher> getDescriptor();
// migrate old JabberPublisher instances
private Object readResolve() {
if (this.strategy == null && this.notificationStrategy != null) {
this.strategy = NotificationStrategy.valueOf(this.notificationStrategy.name());
this.notificationStrategy = null;
}
return this;
}
protected final IMPublisherDescriptor getIMDescriptor() {
return (IMPublisherDescriptor) getDescriptor();
}
/**
* Returns the instant-messaging ID which is configured for a Hudson user
* (e.g. via a {@link UserProperty}) or null if there's nothing configured for
* him/her.
*/
protected abstract String getConfiguredIMId(User user);
}