net.thucydides.plugins.jira.client.JerseyJiraClient Maven / Gradle / Ivy
package net.thucydides.plugins.jira.client;
import com.beust.jcommander.internal.Maps;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import net.thucydides.plugins.jira.domain.IssueSummary;
import net.thucydides.plugins.jira.domain.Version;
import net.thucydides.plugins.jira.model.CascadingSelectOption;
import net.thucydides.plugins.jira.model.CustomField;
import org.glassfish.jersey.client.filter.HttpBasicAuthFilter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.LoggerFactory;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static java.util.Collections.EMPTY_LIST;
/**
* A JIRA client using the new REST interface
*/
@SuppressWarnings("unchecked")
public class JerseyJiraClient {
private static final String REST_SEARCH = "rest/api/latest/search";
private static final String VERSIONS_SEARCH = "rest/api/latest/project/%s/versions";
private static final int REDIRECT_REQUEST = 302;
private static final String DEFAULT_ISSUE_TYPE = "Bug";
private final String url;
private final String username;
private final String password;
private final int batchSize;
private final String project;
private final List customFields;
private Map customFieldsIndex;
private Map customFieldNameIndex;
private String metadataIssueType;
private LoadingCache> issueSummaryCache;
private LoadingCache> issueQueryCache;
private final org.slf4j.Logger logger = LoggerFactory.getLogger(JerseyJiraClient.class);
private final static int DEFAULT_BATCH_SIZE = 100;
private final static int OK = 200;
public JerseyJiraClient(String url, String username, String password, String project) {
this(url, username, password, DEFAULT_BATCH_SIZE, project);
}
public JerseyJiraClient(String url, String username, String password, String project, List customFields) {
this(url, username, password, DEFAULT_BATCH_SIZE, project, DEFAULT_ISSUE_TYPE, customFields);
}
public JerseyJiraClient(String url, String username, String password, int batchSize,
String project,
String metadataIssueType,
List customFields) {
this.url = url;
this.username = username;
this.password = password;
this.batchSize = batchSize;
this.project = project;
this.metadataIssueType = metadataIssueType;
this.customFields = ImmutableList.copyOf(customFields);
this.issueSummaryCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new FindByKeyLoader(this));
this.issueQueryCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new FindByJQLLoader(this));
}
public JerseyJiraClient(String url, String username, String password, int batchSize, String project) {
this(url,username,password,batchSize,project, DEFAULT_ISSUE_TYPE, EMPTY_LIST);
}
public JerseyJiraClient usingCustomFields(List customFields) {
return new JerseyJiraClient(url, username, password, batchSize, project, metadataIssueType, customFields);
}
public JerseyJiraClient usingMetadataIssueType(String metadataIssueType) {
return new JerseyJiraClient(url, username, password, batchSize, project, metadataIssueType, customFields);
}
/**
* Load the issue keys for all of the issues matching the specified JQL query
*
* @param query A valid JQL query
* @return a list of JIRA issue keys
*/
public List findByJQL(String query) throws JSONException {
try {
Preconditions.checkNotNull(query,"JIRA key cannot be null");
return issueQueryCache.get(query);
} catch (ExecutionException e) {
throw new JSONException(e.getCause());
} catch (RuntimeException runtimeException) {
throw new JSONException(runtimeException.getCause());
}
}
protected List loadByJQL(String query) throws JSONException {
int total = countByJQL(query);
List issues = Lists.newArrayList();
int startAt = 0;
while(issues.size() < total) {
String jsonResponse = getJSONResponse(query, startAt);
JSONObject responseObject = new JSONObject(jsonResponse);
JSONArray issueEntries = (JSONArray) responseObject.get("issues");
for (int i = 0; i < issueEntries.length(); i++) {
JSONObject issueObject = issueEntries.getJSONObject(i);
issues.add(convertToIssueSummary(issueObject));
}
startAt = startAt + getBatchSize();
}
return issues;
}
public List findVersionsForProject(String projectName) throws JSONException {
String versionData = getJSONProjectVersions(projectName);
return convertJSONVersions(versionData);
}
private List convertJSONVersions(String versionData) throws JSONException {
List versions = Lists.newArrayList();
JSONArray versionEntries = new JSONArray(versionData);
for (int i = 0; i < versionEntries.length(); i++) {
JSONObject issueObject = versionEntries.getJSONObject(i);
versions.add(convertToVersion(issueObject));
}
return versions;
}
public WebTarget buildWebTargetFor(String path) {
return restClient().target(url).path(path);
}
private String getJSONResponse(String query, int startAt) throws JSONException{
String fields = "key,summary,description,issuetype,labels,fixVersions";
fields = addCustomFieldsTo(fields);
WebTarget target = buildWebTargetFor(REST_SEARCH)
.queryParam("jql", query)
.queryParam("startAt", startAt)
.queryParam("maxResults", batchSize)
.queryParam("expand", "renderedFields")
.queryParam("fields", fields);
Response response = target.request().get();
checkValid(response);
return response.readEntity(String.class);
}
private String addCustomFieldsTo(String fields) throws JSONException {
for(String customField : customFields) {
if (getCustomFieldsIndex().containsKey(customField)) {
fields = fields + "," + getCustomFieldsIndex().get(customField).getId();
}
}
return fields;
}
private String getJSONProjectVersions(String projectName) throws JSONException{
String url = String.format(VERSIONS_SEARCH,projectName);
WebTarget target = buildWebTargetFor(url);
Response response = target.request().get();
checkValid(response);
return response.readEntity(String.class);
}
public Optional findByKey(String key) throws JSONException {
try {
Preconditions.checkNotNull(key,"JIRA key cannot be null");
return issueSummaryCache.get(key);
} catch (ExecutionException e) {
throw new JSONException(e.getCause());
} catch (RuntimeException runtimeException) {
throw new JSONException(runtimeException.getCause());
}
}
public Optional loadByKey(String key) throws JSONException {
Optional jsonResponse = readFieldValues(url, "rest/api/2/issue/" + key);
if (jsonResponse.isPresent()) {
JSONObject responseObject = new JSONObject(jsonResponse.get());
return Optional.of(convertToIssueSummary(responseObject));
}
return Optional.absent();
}
private Version convertToVersion(JSONObject issueObject) throws JSONException {
try {
return new Version(uriFrom(issueObject),
issueObject.getLong("id"),
stringValueOf(issueObject.get("name")),
booleanValueOf(issueObject.get("archived")),
booleanValueOf(issueObject.get("released")));
} catch (JSONException e) {
logger.error("Could not load issue from JSON",e);
logger.error("JSON:" + issueObject.toString(4));
throw e;
}
}
private IssueSummary convertToIssueSummary(JSONObject issueObject) throws JSONException {
JSONObject fields = (JSONObject) issueObject.get("fields");
JSONObject renderedFields = (JSONObject) issueObject.get("renderedFields");
JSONObject issueType = (JSONObject) fields.get("issuetype");
try {
Map renderedFieldValues = renderedFieldValuesFrom(renderedFields);
return new IssueSummary(uriFrom(issueObject),
issueObject.getLong("id"),
stringValueOf(issueObject.get("key")),
stringValueOf(fields.get("summary")),
stringValueOf(optional(fields,"description")),
renderedFieldValues,
stringValueOf(issueType.get("name")),
toList((JSONArray) fields.get("labels")),
toListOfVersions((JSONArray) fields.get("fixVersions")),
customFieldValuesIn(fields,renderedFields));
} catch (JSONException e) {
logger.error("Could not load issue from JSON",e);
logger.error("JSON:" + issueObject.toString(4));
throw e;
}
}
private Map renderedFieldValuesFrom(JSONObject renderedFields) throws JSONException {
Map renderedFieldMap = Maps.newHashMap();
for(Object key : Lists.newArrayList(renderedFields.sortedKeys())) {
String fieldName = (String) key;
String renderedValue = renderedFields.getString(fieldName);
if (getCustomFieldNameIndex().containsKey(fieldName)) {
fieldName = getCustomFieldNameIndex().get(key);
}
renderedFieldMap.put(fieldName, renderedValue);
}
return renderedFieldMap;
}
private Map customFieldValuesIn(JSONObject fields, JSONObject renderedFields) throws JSONException {
Map customFieldValues = Maps.newHashMap();
for (String customFieldName : customFields) {
CustomField customField = getCustomFieldsIndex().get(customFieldName);
if (customFieldDefined(fields, renderedFields, customField)) {
Object customFieldValue = readFieldValue(fields, customField);
customFieldValues.put(customFieldName, customFieldValue);
}
}
return customFieldValues;
}
private boolean customFieldDefined(JSONObject fields, JSONObject renderedFields, CustomField customField) throws JSONException {
if (customField != null) {
return (hasCustomFieldValue(fields, customField) || hasCustomFieldValue(renderedFields, customField));
} else {
return false;
}
}
private boolean hasCustomFieldValue(JSONObject fields, CustomField customField) throws JSONException {
return (fields.has(customField.getId())) && (!fields.get(customField.getId()).equals(null));
}
private Object readFieldValue(JSONObject fields, CustomField customField) throws JSONException {
String fieldId = customField.getId();
String fieldValue = fieldIsDefined(fields, fieldId) ? fields.getString(fieldId) : "";
if (isJSON(fieldValue)) {
JSONObject field = new JSONObject(fieldValue);
if (customField.getType().equals("string")) {
return (field == JSONObject.NULL) ? "" : field.getString("value");
} else if (customField.getType().equals("array")) {
return readListFrom(field);
}
}
return fieldValue;
}
private boolean fieldIsDefined(JSONObject fields, String fieldId) throws JSONException {
return (fields.has(fieldId) && (fields.get(fieldId) != JSONObject.NULL));
}
private boolean isJSON(String fieldValue) {
return fieldValue.trim().startsWith("{");
}
private List readListFrom(JSONObject jsonField) throws JSONException {
List values = Lists.newArrayList();
values.add(jsonField.getString("value"));
if (jsonField.has("child")) {
values.addAll(readListFrom(jsonField.getJSONObject("child")));
}
return values;
}
private List convertToCustomFields(JSONArray customFieldsList) throws JSONException {
List customFields = Lists.newArrayList();
for (int i = 0; i < customFieldsList.length(); i++) {
JSONObject fieldObject = customFieldsList.getJSONObject(i);
customFields.add(convertToCustomField(fieldObject));
}
return customFields;
}
private CustomField convertToCustomField(JSONObject fieldObject) throws JSONException {
return new CustomField(fieldObject.getString("id"),
fieldObject.getString("name"),
fieldTypeOf(fieldObject));
}
private String fieldTypeOf(JSONObject fieldObject) throws JSONException {
if (fieldObject.has("schema")) {
return fieldObject.getJSONObject("schema").getString("type");
} else {
return "string";
}
}
private Object optional(JSONObject fields, String fieldName) throws JSONException {
return (fields.has(fieldName) ? fields.get(fieldName) : null);
}
private List toList(JSONArray array) throws JSONException {
List list = Lists.newArrayList();
for (int i = 0; i < array.length(); i++) {
list.add(stringValueOf(array.get(i)));
}
return list;
}
private List toListOfVersions(JSONArray array) throws JSONException {
List list = Lists.newArrayList();
for (int i = 0; i < array.length(); i++) {
JSONObject versionObject = (JSONObject) array.get(i);
list.add(versionObject.getString("name"));
}
return list;
}
private URI uriFrom(JSONObject issueObject) throws JSONException {
try {
return new URI((String) issueObject.get("self"));
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Self field not a valid URL");
}
}
public Integer countByJQL(String query) throws JSONException{
return loadCountByJQL(query);
// try {
// return issueCountCache.get(query);
// } catch (ExecutionException e) {
// throw new JSONException(e.getCause());
// }
}
protected Integer loadCountByJQL(String query) throws JSONException{
WebTarget target = buildWebTargetFor(REST_SEARCH).queryParam("jql", query);
Response response = target.request().get();
if (isEmpty(response)) {
return 0;
} else {
checkValid(response);
}
String jsonResponse = response.readEntity(String.class);
int total;
try {
JSONObject responseObject = new JSONObject(jsonResponse);
total = (Integer) responseObject.get("total");
} catch (JSONException e) {
throw new IllegalArgumentException("Invalid JQL query: " + query);
}
return total;
}
private Optional readFieldValues(String url, String path) throws JSONException {
WebTarget target = restClient().target(url)
.path(path)
.queryParam("expand", "renderedFields");
Response response = target.request().get();
if (response.getStatus() == REDIRECT_REQUEST) {
response = Redirector.forPath(path).usingClient(restClient()).followRedirectsIn(response);
}
if (resourceDoesNotExist(response)) {
return Optional.absent();
} else {
checkValid(response);
return Optional.of(response.readEntity(String.class));
}
}
private Optional readFieldMetadata(String url, String path) throws JSONException {
WebTarget target = restClient().target(url)
.path(path)
.queryParam("expand", "renderedFields")
.queryParam("project", project)
.queryParam("issuetypeName",metadataIssueType)
.queryParam("expand","projects.issuetypes.fields");
Response response = target.request().get();
if (response.getStatus() == REDIRECT_REQUEST) {
response = Redirector.forPath(path).usingClient(restClient()).followRedirectsIn(response);
}
if (resourceDoesNotExist(response)) {
return Optional.absent();
} else {
checkValid(response);
return Optional.of(response.readEntity(String.class));
}
}
public Client restClient() {
return ClientBuilder.newBuilder().register(new HttpBasicAuthFilter(username, password)).build();
}
private String stringValueOf(Object field) {
if (field != null) {
return field.toString();
} else {
return null;
}
}
private boolean booleanValueOf(Object field) {
if (field != null) {
return Boolean.valueOf(field.toString());
} else {
return false;
}
}
public boolean resourceDoesNotExist(Response response) {
return response.getStatus() == 404;
}
public boolean isEmpty(Response response) {
return response.getStatus() == 400;
}
public void checkValid(Response response) throws JSONException {
int status = response.getStatus();
if (status != OK) {
switch(status) {
case 401 : handleAuthenticationError("Authentication error (401) for user " + this.username);
case 403 : handleAuthenticationError("Forbidden error (403) for user " + this.username);
case 404 : handleConfigurationError("Service not found (404) - try checking the JIRA URL?");
case 407 : handleConfigurationError("Proxy authentication required (407)");
default:
throw new JSONException("JIRA query failed: error " + status);
}
}
}
private void handleAuthenticationError(String message) {
throw new JIRAAuthenticationError(message);
}
private void handleConfigurationError(String message) {
throw new JIRAConfigurationError(message);
}
public int getBatchSize() {
return batchSize;
}
private Map getCustomFieldsIndex() throws JSONException {
if (customFieldsIndex == null) {
customFieldsIndex = indexCustomFields();
}
return customFieldsIndex;
}
private Map getCustomFieldNameIndex() throws JSONException {
if (customFieldNameIndex == null) {
customFieldNameIndex = indexCustomFieldNames();
}
return customFieldNameIndex;
}
private Map indexCustomFieldNames() throws JSONException {
Map index = Maps.newHashMap();
for(CustomField field : getExistingCustomFields()) {
index.put(field.getId(), field.getName());
}
return index;
}
private Map indexCustomFields() throws JSONException {
Map index = Maps.newHashMap();
for(CustomField field : getExistingCustomFields()) {
index.put(field.getName(), field);
}
return index;
}
private List getExistingCustomFields() throws JSONException {
Optional jsonResponse = readFieldValues(url, "rest/api/2/field");
if (jsonResponse.isPresent()) {
JSONArray responseObject = new JSONArray(jsonResponse.get());
return convertToCustomFields(responseObject);
}
return EMPTY_LIST;
}
List getCustomFields() throws JSONException {
List registeredCustomFields = Lists.newArrayList();
for(String fieldName : customFields) {
registeredCustomFields.add(getCustomFieldsIndex().get(fieldName));
}
return registeredCustomFields;
}
public List findOptionsForCascadingSelect(String fieldName) {
JSONObject responseObject = null;
try {
Optional jsonResponse = readFieldMetadata(url, "rest/api/2/issue/createmeta");
if (jsonResponse.isPresent()) {
responseObject = new JSONObject(jsonResponse.get());
JSONObject fields = responseObject.getJSONArray("projects")
.getJSONObject(0)
.getJSONArray("issuetypes")
.getJSONObject(0).getJSONObject("fields");
Iterator fieldKeys = fields.keys();
while(fieldKeys.hasNext()) {
String entryFieldName = (String) fieldKeys.next();
JSONObject entry = fields.getJSONObject(entryFieldName);
if (entry.getString("name").equalsIgnoreCase(fieldName)) {
return convertToCascadingSelectOptions(entry.getJSONArray("allowedValues"));
}
}
}
} catch (JSONException e) {
logger.error("Could not read cascading select options", e);
logger.info("responseObject = " + responseObject);
}
return EMPTY_LIST;
}
private List convertToCascadingSelectOptions(JSONArray allowedValues) throws JSONException {
return convertToCascadingSelectOptions(allowedValues, null);
}
private List convertToCascadingSelectOptions(JSONArray allowedValues,
CascadingSelectOption parentOption) throws JSONException {
List options = Lists.newArrayList();
for(int i = 0; i < allowedValues.length(); i++) {
JSONObject entry = (JSONObject) allowedValues.get(i);
String value = entry.getString("value");
CascadingSelectOption option = new CascadingSelectOption(value, parentOption);
List children = Lists.newArrayList();
if (entry.has("children")) {
children = convertToCascadingSelectOptions(entry.getJSONArray("children"), option);
}
option.addChildren(children);
options.add(option);
}
return options;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy