package hudson.plugins.jira;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.AbstractBuild.DependencyChange;
import hudson.plugins.jira.soap.RemotePermissionException;
import hudson.scm.RepositoryBrowser;
import hudson.scm.ChangeLogSet.AffectedFile;
import hudson.scm.ChangeLogSet.Entry;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.rpc.ServiceException;
import org.apache.commons.lang.StringUtils;
/**
* Actual JIRA update logic.
*
*
* @author Kohsuke Kawaguchi
*/
class Updater {
static boolean perform(AbstractBuild<?, ?> build, BuildListener listener) throws InterruptedException, IOException {
PrintStream logger = listener.getLogger();
List<JiraIssue> issues = null;
try {
JiraSite site = JiraSite.get(build.getProject());
if(site==null) {
logger.println(Messages.Updater_NoJiraSite());
build.setResult(Result.FAILURE);
return true;
}
String rootUrl = Hudson.getInstance().getRootUrl();
if(rootUrl==null) {
logger.println(Messages.Updater_NoHudsonUrl());
build.setResult(Result.FAILURE);
return true;
}
Set<String> ids = findIssueIdsRecursive(build, site.getIssuePattern(), listener);
if(ids.isEmpty()) {
if(debug)
logger.println("No JIRA issues found.");
return true; // nothing found here.
}
JiraSession session = null;
try {
session = site.createSession();
} catch (ServiceException e) {
listener.getLogger().println(Messages.Updater_FailedToConnect());
e.printStackTrace(listener.getLogger());
}
if(session==null) {
logger.println(Messages.Updater_NoRemoteAccess());
build.setResult(Result.FAILURE);
return true;
}
boolean doUpdate = false;
if (site.updateJiraIssueForAllStatus){
doUpdate = true;
} else {
doUpdate = build.getResult().isBetterOrEqualTo(Result.UNSTABLE);
}
boolean useWikiStyleComments = site.supportsWikiStyleComment;
issues = getJiraIssues(ids, session, logger);
build.getActions().add(new JiraBuildAction(build,issues));
if (doUpdate) {
submitComments(build, logger, rootUrl, issues,
session, useWikiStyleComments, site.recordScmChanges, site.groupVisibility);
} else {
// this build didn't work, so carry forward the issues to the next build
build.addAction(new JiraCarryOverAction(issues));
}
} catch (Exception e) {
logger.println("Error updating JIRA issues. Saving issues for next build.\n" + e);
if (issues != null && !issues.isEmpty()) {
// updating issues failed, so carry forward issues to the next build
build.addAction(new JiraCarryOverAction(issues));
}
}
return true;
}
/**
* Submits comments for the given issues.
* Removes from <code>issues</code> the ones which appear to be invalid.
* @param build
* @param logger
* @param hudsonRootUrl
* @param issues
* @param session
* @param useWikiStyleComments
* @param recordScmChanges
* @param groupVisibility
* @throws RemoteException
*/
static void submitComments(
AbstractBuild<?, ?> build, PrintStream logger, String hudsonRootUrl,
List<JiraIssue> issues, JiraSession session,
boolean useWikiStyleComments, boolean recordScmChanges, String groupVisibility) throws RemoteException {
// copy to prevent ConcurrentModificationException
List<JiraIssue> copy = new ArrayList<JiraIssue>(issues);
for (JiraIssue issue : copy) {
try {
logger.println(Messages.Updater_Updating(issue.id));
StringBuilder aggregateComment = new StringBuilder();
for(Entry e :build.getChangeSet()){
if(e.getMsg().toUpperCase().contains(issue.id)){
aggregateComment.append(e.getMsg()).append("\n");
// kutzi: don't know why the issue id was removed in previous versions:
//aggregateComment = aggregateComment.replaceAll(id, "");
}
}
session.addComment(issue.id,
createComment(build, useWikiStyleComments,
hudsonRootUrl, aggregateComment.toString(), recordScmChanges, issue), groupVisibility);
} catch (RemotePermissionException e) {
// Seems like RemotePermissionException can mean 'no permission' as well as
// 'issue doesn't exist'.
// To prevent carrying forward invalid issues forever, we have to drop them
// even if the cause of the exception was different.
logger.println("Looks like " + issue.id + " is no valid JIRA issue. Issue will not be updated.\n" + e);
issues.remove(issue);
}
}
}
private static List<JiraIssue> getJiraIssues(
Set<String> ids, JiraSession session, PrintStream logger) throws RemoteException {
List<JiraIssue> issues = new ArrayList<JiraIssue>(ids.size());
for (String id : ids) {
if(!session.existsIssue(id)) {
if(debug)
logger.println(id+" looked like a JIRA issue but it wasn't");
continue; // token looked like a JIRA issue but it's actually not.
}
issues.add(new JiraIssue(session.getIssue(id)));
}
return issues;
}
/**
* Creates a comment to be used in JIRA for the build.
*/
private static String createComment(AbstractBuild<?, ?> build,
boolean wikiStyle, String hudsonRootUrl, String scmComments, boolean recordScmChanges, JiraIssue jiraIssue) {
String comment = String.format(
wikiStyle ?
"Integrated in !%1$simages/16x16/%3$s! [%2$s|%4$s]\n %5$s":
"Integrated in %2$s (See [%4$s])\n %5$s",
hudsonRootUrl,
build,
build.getResult().color.getImage(),
Util.encode(hudsonRootUrl+build.getUrl()),
scmComments);
if (recordScmChanges) {
List<String> scmChanges = getScmComments(wikiStyle, build, jiraIssue );
StringBuilder sb = new StringBuilder(comment);
for (String scmChange : scmChanges)
{
sb.append( "\n" ).append( scmChange );
}
return sb.toString();
}
return comment;
}
private static List<String> getScmComments(boolean wikiStyle, AbstractBuild<?, ?> build, JiraIssue jiraIssue)
{
RepositoryBrowser repoBrowser = null;
if (build.getProject().getScm() != null) {
repoBrowser = build.getProject().getScm().getEffectiveBrowser();
}
List<String> scmChanges = new ArrayList<String>();
for (Entry change : build.getChangeSet()) {
if (jiraIssue != null && !StringUtils.contains( change.getMsg(), jiraIssue.id )) {
continue;
}
try {
String uid = change.getAuthor().getId();
URL url = repoBrowser == null ? null : repoBrowser.getChangeSetLink( change );
StringBuilder scmChange = new StringBuilder();
if (StringUtils.isNotBlank( uid )) {
scmChange.append( uid ).append( " : " );
}
if (url != null && StringUtils.isNotBlank( url.toExternalForm() )) {
if (wikiStyle) {
String revision = getRevision( change );
if (revision != null)
{
scmChange.append( "[" ).append( revision );
scmChange.append( "|" );
scmChange.append( url.toExternalForm() ).append( "]" );
}
else
{
scmChange.append( "[" ).append( url.toExternalForm() ).append( "]" );
}
} else {
scmChange.append( url.toExternalForm() );
}
}
scmChange.append( "\nFiles : " ).append( "\n" );
for (AffectedFile affectedFile : change.getAffectedFiles()) {
scmChange.append( "* " ).append( affectedFile.getPath() ).append( "\n" );
}
if (scmChange.length()>0) {
scmChanges.add( scmChange.toString() );
}
} catch (IOException e) {
LOGGER.warning( "skip failed to calculate scm repo browser link " + e.getMessage() );
}
}
return scmChanges;
}
private static String getRevision(Entry entry) {
// svn at least can get the revision
try {
Class<?> clazz = entry.getClass();
Method method = clazz.getMethod( "getRevision", (Class[])null );
if (method==null){
return null;
}
Object revObj = method.invoke( entry, (Object[])null );
return (revObj != null) ? revObj.toString() : null;
} catch (Exception e) {
return null;
}
}
/**
* Finds the strings that match JIRA issue ID patterns.
*
* This method returns all likely candidates and doesn't check
* if such ID actually exists or not. We don't want to use
* {@link JiraSite#existsIssue(String)} here so that new projects
* in JIRA can be detected.
*/
private static Set<String> findIssueIdsRecursive(AbstractBuild<?,?> build, Pattern pattern,
BuildListener listener) {
Set<String> ids = new HashSet<String>();
// first, issues that were carried forward.
Run<?, ?> prev = build.getPreviousBuild();
if(prev!=null) {
JiraCarryOverAction a = prev.getAction(JiraCarryOverAction.class);
if(a!=null)
ids.addAll(a.getIDs());
}
// then issues in this build
findIssues(build,ids, pattern, listener);
// check for issues fixed in dependencies
for( DependencyChange depc : build.getDependencyChanges(build.getPreviousBuild()).values())
for(AbstractBuild<?, ?> b : depc.getBuilds())
findIssues(b,ids, pattern, listener);
return ids;
}
/**
* @param pattern pattern to use to match issue ids
*/
static void findIssues(AbstractBuild<?,?> build, Set<String> ids, Pattern pattern,
BuildListener listener) {
for (Entry change : build.getChangeSet()) {
LOGGER.fine("Looking for JIRA ID in "+change.getMsg());
Matcher m = pattern.matcher(change.getMsg());
while (m.find()) {
if (m.groupCount() >= 1) {
String content = StringUtils.upperCase( m.group(1));
ids.add(content);
} else {
listener.getLogger().println("Warning: The JIRA pattern " + pattern + " doesn't define a capturing group!");
}
}
}
}
private static final Logger LOGGER = Logger.getLogger(Updater.class.getName());
/**
* Debug flag.
*/
public static boolean debug = false;
}