package hudson.plugins.jira;
import hudson.Util;
import hudson.model.AbstractProject;
import hudson.plugins.jira.soap.JiraSoapService;
import hudson.plugins.jira.soap.JiraSoapServiceService;
import hudson.plugins.jira.soap.JiraSoapServiceServiceLocator;
import hudson.plugins.jira.soap.RemoteIssue;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.xml.rpc.ServiceException;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* Represents an external JIRA installation and configuration
* needed to access this JIRA.
*
* @author Kohsuke Kawaguchi
*/
public class JiraSite {
/**
* Regexp pattern that identifies JIRA issue token.
* If this pattern changes help pages (help-issue-pattern_xy.html) must be updated
* <p>
* First char must be a letter, then at least one letter, digit or underscore.
* See issue HUDSON-729, HUDSON-4092
*/
protected static final Pattern DEFAULT_ISSUE_PATTERN = Pattern.compile("([a-zA-Z][a-zA-Z0-9_]+-[1-9][0-9]*)([^.]|\\.[^0-9]|\\.$|$)");
/**
* URL of JIRA, like <tt>http://jira.codehaus.org/</tt>.
* Mandatory. Normalized to end with '/'
*/
public final URL url;
/**
* User name needed to login. Optional.
*/
public final String userName;
/**
* Password needed to login. Optional.
*/
public final String password;
/**
* Group visibility to constrain the visibility of the added comment. Optional.
*/
public final String groupVisibility;
/**
* True if this JIRA is configured to allow Confluence-style Wiki comment.
*/
public final boolean supportsWikiStyleComment;
/**
* to record scm changes in jira issue
* @since 1.21
*/
public final boolean recordScmChanges;
/**
* user defined pattern
* @since 1.22
*/
private final String userPattern;
private transient Pattern userPat;
/**
* updated jira issue for all status
* @since 1.22
*/
public final boolean updateJiraIssueForAllStatus;
/**
* List of project keys (i.e., "MNG" portion of "MNG-512"),
* last time we checked. Copy on write semantics.
*/
// TODO: seems like this is never invalidated (never set to null)
// should we implement to invalidate this (say every hour)?
private transient volatile Set<String> projects;
/**
* @stapler-constructor
*/
@DataBoundConstructor
public JiraSite(URL url, String userName, String password, boolean supportsWikiStyleComment, boolean recordScmChanges, String userPattern,
boolean updateJiraIssueForAllStatus, String groupVisibility) {
if(!url.toExternalForm().endsWith("/"))
try {
url = new URL(url.toExternalForm()+"/");
} catch (MalformedURLException e) {
throw new AssertionError(e); // impossible
}
this.url = url;
this.userName = Util.fixEmpty(userName);
this.password = Util.fixEmpty(password);
this.supportsWikiStyleComment = supportsWikiStyleComment;
this.recordScmChanges = recordScmChanges;
this.userPattern = Util.fixEmpty(userPattern);
if (this.userPattern != null) {
this.userPat = Pattern.compile(this.userPattern);
} else {
this.userPat = null;
}
this.updateJiraIssueForAllStatus = updateJiraIssueForAllStatus;
this.groupVisibility = Util.fixEmpty(groupVisibility);
}
public String getName() {
return url.toExternalForm();
}
/**
* Creates a remote access session to this JIRA.
*
* @return
* null if remote access is not supported.
*/
public JiraSession createSession() throws IOException, ServiceException {
if(userName==null || password==null)
return null; // remote access not supported
JiraSoapServiceService jiraSoapServiceGetter = new JiraSoapServiceServiceLocator();
JiraSoapService service = jiraSoapServiceGetter.getJirasoapserviceV2(
new URL(url, "rpc/soap/jirasoapservice-v2"));
return new JiraSession(this,service,service.login(userName,password));
}
/**
* Computes the URL to the given issue.
*/
public URL getUrl(JiraIssue issue) throws IOException {
return getUrl(issue.id);
}
/**
* Computes the URL to the given issue.
*/
public URL getUrl(String id) throws MalformedURLException {
return new URL(url, "browse/" + id.toUpperCase());
}
/**
* Gets the user-defined issue pattern if any.
*
* @return the pattern or null
*/
public Pattern getUserPattern() {
if (userPattern == null) {
return null;
}
if (userPat == null) {
// We don't care about any thread race- or visibility issues here.
// The worst thing which could happen, is that the pattern
// is compiled multiple times.
Pattern p = Pattern.compile(userPattern);
userPat = p;
}
return userPat;
}
public Pattern getIssuePattern() {
if (getUserPattern() != null) {
return getUserPattern();
}
return DEFAULT_ISSUE_PATTERN;
}
/**
* Gets the list of project IDs in this JIRA.
* This information could be bit old, or it can be null.
*/
public Set<String> getProjectKeys() {
if(projects==null) {
synchronized (this) {
try {
if(projects==null) {
JiraSession session = createSession();
if(session!=null)
projects = Collections.unmodifiableSet(session.getProjectKeys());
}
} catch (IOException e) {
// in case of error, set empty set to avoid trying the same thing repeatedly.
LOGGER.log(Level.WARNING,"Failed to obtain JIRA project list",e);
} catch (ServiceException e) {
LOGGER.log(Level.WARNING,"Failed to obtain JIRA project list",e);
}
}
}
// fall back to empty if failed to talk to the server
if(projects==null) {
return Collections.emptySet();
}
return projects;
}
/**
* Gets the effective {@link JiraSite} associated with the given project.
*
* @return null
* if no such was found.
*/
public static JiraSite get(AbstractProject<?,?> p) {
JiraProjectProperty jpp = p.getProperty(JiraProjectProperty.class);
if(jpp!=null) {
JiraSite site = jpp.getSite();
if(site!=null)
return site;
}
// none is explicitly configured. try the default ---
// if only one is configured, that must be it.
JiraSite[] sites = JiraProjectProperty.DESCRIPTOR.getSites();
if(sites.length==1) return sites[0];
return null;
}
/**
* Checks if the given JIRA id will be likely to exist in this issue tracker.
*
* <p>
* This method checks whether the key portion is a valid key (except that
* it can potentially use stale data). Number portion is not checked at all.
*
* @param id
* String like MNG-1234
*/
public boolean existsIssue(String id) {
int idx = id.indexOf('-');
if(idx==-1) return false;
Set<String> keys = getProjectKeys();
return keys.contains(id.substring(0,idx).toUpperCase());
}
/**
* Returns the remote issue with the given id or <code>null</code> if it wasn't found.
*/
public JiraIssue getIssue(String id) throws IOException, ServiceException {
JiraSession session = createSession();
if (session != null) {
RemoteIssue remoteIssue = session.getIssue(id);
if (remoteIssue != null) {
return new JiraIssue(remoteIssue);
}
}
return null;
}
private static final Logger LOGGER = Logger.getLogger(JiraSite.class.getName());
}