com.seleniumtests.connectors.bugtracker.jira.JiraConnector Maven / Gradle / Ivy
package com.seleniumtests.connectors.bugtracker.jira;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.atlassian.jira.rest.client.api.IssueRestClient;
import com.atlassian.jira.rest.client.api.JiraRestClient;
import com.atlassian.jira.rest.client.api.RestClientException;
import com.atlassian.jira.rest.client.api.SearchRestClient;
import com.atlassian.jira.rest.client.api.domain.BasicComponent;
import com.atlassian.jira.rest.client.api.domain.BasicIssue;
import com.atlassian.jira.rest.client.api.domain.BasicPriority;
import com.atlassian.jira.rest.client.api.domain.BasicProject;
import com.atlassian.jira.rest.client.api.domain.CimFieldInfo;
import com.atlassian.jira.rest.client.api.domain.Comment;
import com.atlassian.jira.rest.client.api.domain.CustomFieldOption;
import com.atlassian.jira.rest.client.api.domain.Field;
import com.atlassian.jira.rest.client.api.domain.Issue;
import com.atlassian.jira.rest.client.api.domain.IssueType;
import com.atlassian.jira.rest.client.api.domain.Priority;
import com.atlassian.jira.rest.client.api.domain.Project;
import com.atlassian.jira.rest.client.api.domain.Transition;
import com.atlassian.jira.rest.client.api.domain.User;
import com.atlassian.jira.rest.client.api.domain.Version;
import com.atlassian.jira.rest.client.api.domain.input.IssueInput;
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder;
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput;
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory;
import com.google.common.collect.ImmutableList;
import com.seleniumtests.connectors.bugtracker.BugTracker;
import com.seleniumtests.connectors.bugtracker.IssueBean;
import com.seleniumtests.core.SeleniumTestsContextManager;
import com.seleniumtests.customexception.ConfigurationException;
import com.seleniumtests.customexception.ScenarioException;
import com.seleniumtests.driver.screenshots.ScreenShot;
import com.seleniumtests.reporter.logger.Snapshot;
import com.seleniumtests.reporter.logger.TestStep;
import com.seleniumtests.util.helper.WaitHelper;
public class JiraConnector extends BugTracker {
private static final String ITEM = " - %s";
private static Logger logger = Logger.getLogger(JiraConnector.class);
private String projectKey;
private String browseUrl;
private Map components;
private Map issueTypes;
private Map priorities;
private Map fields;
private Map versions;
private JiraRestClient restClient;
private Project project;
List openStates;
String closeTransition;
/**
* Constructor only there to allow getting information about a project
* @param server exemple: http://jira.covea.priv
* @param user
* @param password
*/
public JiraConnector(String server, String projectKey, String user, String password) {
this.projectKey = projectKey;
if (server == null || server.isEmpty()
|| projectKey == null || projectKey.isEmpty()
|| user == null || user.isEmpty()
|| password == null || password.isEmpty()) {
throw new ConfigurationException("Missing configuration for Jira, please provide 'bugtrackerUrl', 'bugtrackerProject', 'bugtrackerUser' and bugtrackerPassword' parameters");
}
try {
restClient = new AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(new URI(server), user, password);
} catch (URISyntaxException e) {
throw new ConfigurationException(String.format("Jira URL %s is incorrect", server));
}
try {
project = restClient.getProjectClient().getProject(projectKey).claim();
} catch (RestClientException e) {
for (BasicProject project: restClient.getProjectClient().getAllProjects().claim()) {
logger.info(project.getKey());
}
throw new ConfigurationException(String.format("Project with key '%s' cannot be found on jira server %s", projectKey, server));
}
browseUrl = server + "/browse/";
components = getComponents();
issueTypes = getIssueTypes();
priorities = getPriorities();
fields = getCustomFields();
versions = getVersions();
}
/**
* constructor to call for creating new issues
* @param server exemple: http://jira.covea.priv
* @param user
* @param password
*/
public JiraConnector(String server, String projectKey, String user, String password, Map jiraOptions) {
this(server, projectKey, user, password);
// check jira options
closeTransition = jiraOptions.get("jira.closeTransition");
if (jiraOptions.get("jira.openStates") == null) {
throw new ConfigurationException("'bugtracker.jira.openStates' MUST be set. It's the state of an issue when it has juste been create. Used to search for open issues");
} else {
openStates = Arrays.asList(jiraOptions.get("jira.openStates").split(",")).stream().map(String::trim).collect(Collectors.toList());
}
if (closeTransition == null) {
throw new ConfigurationException("'bugtracker.jira.closeTransition' MUST be set. It's the name of the transition that will close an issue");
}
logger.info(String.format("Connecting to Jira API [%s], on project [%s] with user [%s]", server, projectKey, user));
}
private Map getComponents() {
Map jiraComponents = new HashMap<>();
project.getComponents().forEach(basicComponent -> jiraComponents.put(basicComponent.getName(), basicComponent));
return jiraComponents;
}
private Map getIssueTypes() {
Map jiraIssueTypes = new HashMap<>();
project.getIssueTypes().forEach(issueType -> jiraIssueTypes.put(issueType.getName(), issueType));
return jiraIssueTypes;
}
private Map getVersions() {
Map jiraVersions = new HashMap<>();
project.getVersions().forEach(version -> jiraVersions.put(version.getName(), version));
return jiraVersions;
}
private Map getPriorities() {
Map jiraPriorities = new HashMap<>();
restClient.getMetadataClient().getPriorities().claim().forEach(priority -> jiraPriorities.put(priority.getName(), priority));
return jiraPriorities;
}
private Map getCustomFields() {
Map jiraFields = new HashMap<>();
restClient.getMetadataClient().getFields().claim().forEach(field -> jiraFields.put(field.getName(), field));
return jiraFields;
}
private Map getCustomFieldInfos(Project project, IssueType issueType) {
Map jiraFieldValues = new HashMap<>();
restClient.getIssueClient().getCreateIssueMetaFields(project.getKey(), issueType.getId().toString(), null, null)
.claim()
.getValues()
.forEach(fieldInfo -> jiraFieldValues.put(fieldInfo.getName(), fieldInfo));
return jiraFieldValues;
}
private User getUser(String email) {
try {
return ImmutableList.copyOf(restClient.getUserClient().findUsers(email).claim()).get(0);
} catch (IndexOutOfBoundsException | RestClientException e) {
return null;
}
}
/**
* Check if issue already exists, and if so, returns an updated IssueBean
* @param issueBean a JiraBean instance
*
* @return
*/
@Override
public IssueBean issueAlreadyExists(IssueBean issueBean) {
if (!(issueBean instanceof JiraBean)) {
throw new ClassCastException("JiraConnector needs JiraBean instances");
}
JiraBean jiraBean = (JiraBean)issueBean;
String jql = String.format("project=%s and summary ~ \"%s\" and status in (\"%s\")", projectKey, jiraBean.getSummary()
.replace("[", "\\\\[")
.replace("]", "\\\\]")
, StringUtils.join(openStates, "\",\"")
);
SearchRestClient searchClient = restClient.getSearchClient();
List issues = ImmutableList.copyOf(searchClient.searchJql(jql).claim().getIssues());
if (!issues.isEmpty()) {
Issue issue = issues.get(0);
JiraBean updatedJiraBean = new JiraBean(issue.getKey(),
jiraBean.getSummary(),
issue.getDescription(),
jiraBean.getPriority(),
jiraBean.getIssueType(),
jiraBean.getTestName(),
jiraBean.getTestStep(),
jiraBean.getAssignee(),
jiraBean.getReporter(),
jiraBean.getScreenShots(),
jiraBean.getDetailedResult(),
jiraBean.getCustomFields(),
jiraBean.getComponents());
updatedJiraBean.setDate(issue.getCreationDate().toString("yyyy-MM-dd'T'HH:mmZZ"));
updatedJiraBean.setAccessUrl(browseUrl + issue.getKey());
return updatedJiraBean;
} else {
return null;
}
}
/**
* Creates the bean effectively
*/
@Override
protected IssueBean createIssueBean(
String summary,
String fullDescription,
String testName,
TestStep lastStep,
String assignee,
String reporter,
List screenShots,
File zipFile,
Map issueOptions) {
String priority = issueOptions.get("priority");
String issueType = issueOptions.get("jira.issueType");
Map customFieldsValues = new HashMap<>();
for (Entry variable: issueOptions.entrySet()) {
if (variable.getKey().startsWith("jira.field.")) {
customFieldsValues.put(variable.getKey().replace("jira.field.", ""), variable.getValue());
}
}
List comps = new ArrayList<>();
if (issueOptions.get("jira.components") != null) {
comps = Arrays.asList(issueOptions.get("jira.components").split(","));
}
return new JiraBean(summary,
fullDescription,
priority,
issueType,
testName,
lastStep,
assignee,
reporter,
screenShots,
zipFile,
customFieldsValues,
comps);
}
/**
* Format description of the issue
*/
@Override
protected void formatDescription(String testName, List failedSteps, TestStep lastTestStep, String description, StringBuilder fullDescription) {
fullDescription.append(String.format("*Test:* %s\n", testName));
if (description != null) {
fullDescription.append(String.format("*Description:* %s\n", description.replace("Test goal:", "*Test goal:*")));
}
if (SeleniumTestsContextManager.getThreadContext().getStartedBy() != null) {
fullDescription.append(String.format("*Started by:* %s\n", SeleniumTestsContextManager.getThreadContext().getStartedBy()));
}
for (TestStep failedStep: failedSteps) {
fullDescription.append(String.format("*Error step #%d (%s):* *{color:#de350b}%s{color}*\n", failedStep.getPosition(), failedStep.getName(), failedStep.getActionException()));
}
formatFailedStep(failedSteps, fullDescription);
fullDescription.append("h2. Last logs\n");
if (lastTestStep != null) {
fullDescription.append(String.format("{code:java}%s{code}", lastTestStep.toString()));
fullDescription.append("\n\nh2. Associated screenshots\n");
List screenshots = lastTestStep.getSnapshots().stream()
.map(Snapshot::getScreenshot)
.collect(Collectors.toList());
for (ScreenShot screenshot: screenshots) {
if (screenshot.getFullImagePath() != null && new File(screenshot.getFullImagePath()).exists()) {
fullDescription.append(String.format("!%s|thumbnail!\n", new File(screenshot.getFullImagePath()).getName()));
}
}
}
}
/**
* @param failedSteps
* @param fullDescription
*/
private void formatFailedStep(List failedSteps, StringBuilder fullDescription) {
if (!failedSteps.isEmpty()) {
fullDescription.append("h2. Steps in error\n");
for (TestStep failedStep: failedSteps) {
fullDescription.append(String.format("* *" + STEP_KO_PATTERN + "%s*\n", failedStep.getPosition(), failedStep.getName().trim()));
if (failedStep.getRootCause() != null) {
fullDescription.append(String.format("+Possible cause:+ %s%s\n", failedStep.getRootCause(), failedStep.getRootCauseDetails() == null || failedStep.getRootCauseDetails().trim().isEmpty() ? "": " => " + failedStep.getRootCauseDetails()));
}
fullDescription.append(String.format("{code:java}%s{code}\n\n", failedStep.toString()));
}
}
}
/**
* Create issue
*/
public void createIssue(IssueBean issueBean) {
if (!(issueBean instanceof JiraBean)) {
throw new ClassCastException("JiraConnector needs JiraBean instances");
}
JiraBean jiraBean = (JiraBean)issueBean;
IssueType issueType = issueTypes.get(jiraBean.getIssueType());
if (issueType == null) {
throw new ConfigurationException(String.format("Issue type %s cannot be found among valid issue types %s", jiraBean.getIssueType(), issueTypes.keySet()));
}
Map fieldInfos = getCustomFieldInfos(project, issueType);
IssueRestClient issueClient = restClient.getIssueClient();
IssueInputBuilder issueBuilder = new IssueInputBuilder(project, issueType, jiraBean.getSummary())
.setDescription(jiraBean.getDescription())
;
if (isDueDateRequired(fieldInfos)) {
issueBuilder.setDueDate(jiraBean.getJodaDateTime());
}
if (jiraBean.getAssignee() != null && !jiraBean.getAssignee().isEmpty()) {
User user = getUser(jiraBean.getAssignee());
if (user == null) {
throw new ConfigurationException(String.format("Assignee %s cannot be found among jira users", jiraBean.getAssignee()));
}
issueBuilder.setAssignee(user);
}
if (jiraBean.getPriority() != null && !jiraBean.getPriority().isEmpty()) {
Priority priority = priorities.get(jiraBean.getPriority());
if (priority == null) {
throw new ConfigurationException(String.format("Priority %s cannot be found on this jira project, valid priorities are %s", jiraBean.getPriority(), priorities.keySet()));
}
issueBuilder.setPriority(priority);
}
if (jiraBean.getReporter() != null && !jiraBean.getReporter().isEmpty()) {
issueBuilder.setReporterName(jiraBean.getReporter());
}
// set fields
setCustomFields(jiraBean, fieldInfos, issueBuilder);
// set components
issueBuilder.setComponents(jiraBean.getComponents()
.stream()
.filter(component -> components.get(component) != null)
.map(component -> components.get(component))
.collect(Collectors.toList())
.toArray(new BasicComponent[] {}));
// add issue
IssueInput newIssue = issueBuilder.build();
BasicIssue basicIssue = issueClient.createIssue(newIssue).claim();
Issue issue = issueClient.getIssue(basicIssue.getKey()).claim();
addAttachments(jiraBean, issueClient, issue);
jiraBean.setId(issue.getKey());
jiraBean.setAccessUrl(browseUrl + issue.getKey());
}
/**
* Add attachments to the issue
* @param jiraBean
* @param issueClient
* @param issue
*/
private void addAttachments(JiraBean jiraBean, IssueRestClient issueClient, Issue issue) {
if (!jiraBean.getScreenShots().isEmpty()) {
File[] files = jiraBean.getScreenShots()
.stream()
.peek(s -> logger.info("file -> " + s.getFullImagePath()))
.map(s -> new File(s.getFullImagePath()))
.filter(File::exists)
.collect(Collectors.toList())
.toArray(new File[] {});
if (files.length > 0) {
issueClient.addAttachments(issue.getAttachmentsUri(), files).claim();
}
}
if (jiraBean.getDetailedResult() != null && jiraBean.getDetailedResult().exists()) {
issueClient.addAttachments(issue.getAttachmentsUri(), jiraBean.getDetailedResult()).claim();
}
}
/**
* Add custom fields to the issue
* @param jiraBean
* @param fieldInfos
* @param issueBuilder
*/
private void setCustomFields(JiraBean jiraBean, Map fieldInfos, IssueInputBuilder issueBuilder) {
jiraBean.getCustomFields().forEach((fieldName, fieldValue) -> {
if (fieldInfos.get(fieldName) != null && fields.get(fieldName) != null) {
if ("option".equals(fields.get(fieldName).getSchema().getType())) {
CustomFieldOption option = getOptionForField(fieldInfos, fieldName, fieldValue);
if (option == null) {
logger.warn(String.format("Value %s for field %s does not exist", fieldValue, fieldName));
} else {
issueBuilder.setFieldValue(fields.get(fieldName).getId(), option);
}
} else if ("string".equals(fields.get(fieldName).getSchema().getType())) {
issueBuilder.setFieldValue(fields.get(fieldName).getId(), fieldValue);
} else {
logger.warn(String.format("Field %s type cannot be handled", fieldName));
}
} else {
logger.warn(String.format("Field %s does not exist", fieldName));
}
});
}
/**
* Search the right field option among all allowed values or return null if value is not valid or field cannot be found
* @param fieldInfos
* @param fieldName
* @param fieldValue
* @return
*/
private CustomFieldOption getOptionForField(Map fieldInfos, String fieldName, String fieldValue) {
CimFieldInfo fieldInfo = fieldInfos.get(fieldName);
if (fieldInfo == null) {
return null;
} else {
for (Object obj: fieldInfo.getAllowedValues()) {
if (obj instanceof CustomFieldOption && ((CustomFieldOption)obj).getValue().equals(fieldValue)) {
return (CustomFieldOption)obj;
}
}
}
return null;
}
private boolean isDueDateRequired(Map fieldInfos) {
for (CimFieldInfo fieldInfo: fieldInfos.values()) {
if ("duedate".equals(fieldInfo.getSchema().getSystem()) && fieldInfo.isRequired()) {
return true;
}
}
return false;
}
private Map> getRequiredFieldsAndValues(Map fieldInfos) {
Map> requiredFields = new HashMap<>();
for (Entry entry: fieldInfos.entrySet()) {
// exclude some "mandatory" fields from list as they are always provided
if (Arrays.asList("project", "summary", "description", "issuetype").contains(entry.getValue().getSchema().getSystem())) {
continue;
}
if (entry.getValue().isRequired()
&& ("option".equals(entry.getValue().getSchema().getType())
|| "array".equals(entry.getValue().getSchema().getType())
)
) {
List allowedValues = new ArrayList<>();
entry.getValue().getAllowedValues().forEach(v -> {
if (v instanceof CustomFieldOption) {
allowedValues.add(((CustomFieldOption)v).getValue());
} else if (v instanceof BasicComponent) {
allowedValues.add(((BasicComponent)v).getName());
} else if (v instanceof BasicPriority) {
allowedValues.add(((BasicPriority)v).getName());
}
});
requiredFields.put(entry.getKey(), allowedValues);
} else if (entry.getValue().isRequired()) {
requiredFields.put(entry.getKey(), new ArrayList<>());
}
}
return requiredFields;
}
/**
* Close issue, applying all necessary transitions
* @param issueId ID of the issue
* @param closingMessage Message of closing
*/
public void closeIssue(String issueId, String closingMessage){
logger.info(String.format("closing issue %s", issueId));
IssueRestClient issueClient = restClient.getIssueClient();
Issue issue;
try {
issue = issueClient.getIssue(issueId).claim();
} catch (RestClientException e) {
throw new ScenarioException(String.format("Jira issue %s does not exist, cannot close it", issueId));
}
Map transitions = new HashMap<>();
issueClient.getTransitions(issue).claim().forEach(transition -> transitions.put(transition.getName(), transition));
List closeWorkflow = Arrays.asList(closeTransition.split("/"));
int workflowPosition = -1;
for (String transitionName: transitions.keySet()) {
workflowPosition = closeWorkflow.indexOf(transitionName);
if (workflowPosition != -1) {
break;
}
}
if (workflowPosition == -1) {
throw new ConfigurationException(String.format("'bugtracker.jira.closeTransition' values [%s] are unknown for this issue, allowed transitions are %s", closeTransition, transitions.keySet()));
} else {
for (String transitionName: closeWorkflow.subList(workflowPosition, closeWorkflow.size())) {
transitionIssue(issueClient, issue, transitions, transitionName);
}
}
}
/**
* Transition issue
* @param issueClient
* @param issue
* @param transitions
* @param transitionName
*/
private void transitionIssue(IssueRestClient issueClient, Issue issue, Map transitions,
String transitionName) {
for (int i = 0; i < 5; i++) {
if (transitions.get(transitionName) == null) {
// update available transitions
WaitHelper.waitForMilliSeconds(500);
transitions.clear();
issueClient.getTransitions(issue).claim().forEach(transition -> transitions.put(transition.getName(), transition));
continue;
}
issueClient.transition(issue, new TransitionInput(transitions.get(transitionName).getId(), new ArrayList<>()));
return;
}
throw new ConfigurationException(String.format("'bugtracker.jira.closeTransition': value [%s] is invalid for this issue in its current state, allowed transitions are %s", transitionName, transitions.keySet()));
}
/**
* Create description for issue update
* @param messageUpdate
* @param screenShots
* @param lastFailedStep
* @return
*/
private StringBuilder formatUpdateDescription(String messageUpdate, List screenShots, TestStep lastFailedStep) {
StringBuilder fullDescription = new StringBuilder(messageUpdate + "\n");
if (SeleniumTestsContextManager.getThreadContext().getStartedBy() != null) {
fullDescription.append(String.format("*Started by:* %s\n", SeleniumTestsContextManager.getThreadContext().getStartedBy()));
}
if (lastFailedStep != null) {
fullDescription.append(String.format("*" + STEP_KO_PATTERN + "%s*\n", lastFailedStep.getPosition(), lastFailedStep.getName().trim()));
fullDescription.append(String.format("{code:java}%s{code}\n\n", lastFailedStep.toString()));
}
// add snapshots to comment
for (ScreenShot screenshot: screenShots) {
fullDescription.append(String.format("!%s|thumbnail!\n", new File(screenshot.getFullImagePath()).getName()));
}
return fullDescription;
}
/**
* Update an existing jira issue with a new message and new screenshots.
* Used when an issue has already been raised, we complete it
* @param issueId Id of the issue
* @param messageUpdate message to add to description
* @param screenShots New screenshots
*/
@Override
public void updateIssue(String issueId, String messageUpdate, List screenShots) {
updateIssue(issueId, messageUpdate, screenShots, null);
}
@Override
public void updateIssue(String issueId, String messageUpdate, List screenShots, TestStep lastFailedStep) {
IssueRestClient issueClient = restClient.getIssueClient();
Issue issue;
try{
issue = issueClient.getIssue(issueId).claim();
} catch (RestClientException e) {
throw new ScenarioException(String.format("Jira issue %s does not exist, cannot update it", issueId));
}
try {
if (!screenShots.isEmpty()) {
issueClient.addAttachments(issue.getAttachmentsUri(), screenShots
.stream()
.peek(s -> logger.info("file ->" + s.getFullImagePath()))
.map(s -> new File(s.getFullImagePath()))
.collect(Collectors.toList())
.toArray(new File[] {})
);
}
// add comment
issueClient.addComment(issue.getCommentsUri(), Comment.valueOf(formatUpdateDescription(messageUpdate, screenShots, lastFailedStep).toString()));
logger.info(String.format("Jira %s updated", issueId));
} catch (Exception e) {
logger.error(String.format("Jira %s not modified: %s", issueId, e.getMessage()));
throw e;
}
}
public List getOpenStates() {
return openStates;
}
/**
* Method for getting required fields and allowed values for creating an issue on a project
*
* @param args
* server => url of jira server
* user => user to connect to jira
* password => password to connect to jira
* project => projectkey
* issueType => type of the issue that will be created
* issueKey (optional) => give the transitions for a specific issue
*
*/
public static void main(String[] args) {
if (args.length < 5) {
System.out.println("Usage: JiraConnector ");
System.exit(1);
}
JiraConnector jiraConnector = new JiraConnector(args[0], args[1], args[2], args[3]);
IssueType issueType = jiraConnector.issueTypes.get(args[4]);
if (issueType == null) {
throw new ConfigurationException(String.format("Issue type %s cannot be found among valid issue types %s", args[4], jiraConnector.issueTypes.keySet()));
}
System.out.println("Proprities:");
for (String priority: jiraConnector.priorities.keySet()) {
System.out.println(String.format(ITEM, priority));
}
System.out.println("\nComponents:");
for (String component: jiraConnector.components.keySet()) {
System.out.println(String.format(ITEM, component));
}
System.out.println(String.format("\nListing required fields and allowed values (if any) for issue '%s'", args[4]));
Map fieldInfos = jiraConnector.getCustomFieldInfos(jiraConnector.project, issueType);
for (Entry> entry: jiraConnector.getRequiredFieldsAndValues(fieldInfos).entrySet()) {
System.out.println(String.format("Field '%s':", entry.getKey()));
for (String value: entry.getValue()) {
System.out.println(String.format(ITEM, value));
}
}
if (args.length >= 6) {
IssueRestClient issueClient = jiraConnector.restClient.getIssueClient();
Issue issue;
try {
issue = issueClient.getIssue(args[5]).claim();
} catch (RestClientException e) {
throw new ScenarioException(String.format("Jira issue %s does not exist, cannot close it", args[5]));
}
Map transitions = new HashMap<>();
issueClient.getTransitions(issue).claim().forEach(transition -> transitions.put(transition.getName(), transition));
System.out.println(transitions);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy