diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java index 0722e94..044e1ce 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java @@ -3,6 +3,8 @@ public class JIRAConstants { + public static final String JIRA_NAME_PREFIX = "#@#name#@#"; + public static final String NULL_VALUE = ""; public static final String REPLACE_PROJECT_KEY = "%PROJECT_KEY%"; diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRARequestIntegration.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRARequestIntegration.java index 574c846..cb89d35 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRARequestIntegration.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRARequestIntegration.java @@ -9,6 +9,8 @@ import java.util.*; +import static com.ppm.integration.agilesdk.connector.jira.JIRAConstants.JIRA_NAME_PREFIX; + public class JIRARequestIntegration extends RequestIntegration { private JIRAServiceProvider service = new JIRAServiceProvider().useAdminAccount(); @@ -33,23 +35,25 @@ public class JIRARequestIntegration extends RequestIntegration { { List fieldsInfo = new ArrayList<>(); - List fields = service.get(instanceConfigurationParameters).getFields(agileProjectValue, entityType); + List fields = new ArrayList(service.get(instanceConfigurationParameters).getFields(agileProjectValue, entityType).values()); for (JIRAFieldInfo field: fields) { // We support following JIRA fields types to map: // - string // - number - // - array type with non-empty allowedValues + // - array type with non-empty allowedValues (i.e. drop down list) + // - User or array of User // - priority (provided that allowedValues is non-empty) if ("string".equals(field.getType()) || "number".equals(field.getType()) + || "user".equals(field.getType()) || "priority".equals(field.getType()) || ("array".equals(field.getType()))) { - if (field.isList() && (field.getAllowedValues()== null || field.getAllowedValues().isEmpty())) { - // We only allow to select lists that have some value options. + if (field.isList() && !"user".equals(field.getType()) && (field.getAllowedValues()== null || field.getAllowedValues().isEmpty())) { + // We only allow to select lists that have some static value options or are users lists. continue; } @@ -57,6 +61,7 @@ public class JIRARequestIntegration extends RequestIntegration { fieldInfo.setId(field.getKey()); fieldInfo.setLabel(field.getName()); fieldInfo.setListType(field.isList()); + fieldInfo.setFieldType(field.getType()); fieldsInfo.add(fieldInfo); } } @@ -64,9 +69,11 @@ public class JIRARequestIntegration extends RequestIntegration { return fieldsInfo; } - @Override public List getAgileEntityFieldValueList(String agileProjectValue, String entityType, String listIdentifier, + /*@Override public List getAgileEntityFieldValueList(String agileProjectValue, String entityType, String listIdentifier, ValueSet instanceConfigurationParameters) { + // Not used. + List fields = service.get(instanceConfigurationParameters).getFields(agileProjectValue, entityType); for (JIRAFieldInfo field : fields) { @@ -77,35 +84,61 @@ public class JIRARequestIntegration extends RequestIntegration { // Field not found. return new ArrayList(); - } + }*/ @Override public AgileEntity updateEntity(String agileProjectValue, String entityType, AgileEntity entity, ValueSet instanceConfigurationParameters) { - Map fields = getFieldsFromAgileEntity(entity); JIRAServiceProvider.JIRAService jiraService = service.get(instanceConfigurationParameters); + Map fields = getFieldsFromAgileEntity(entity, jiraService); + + String issueKey = jiraService.updateIssue(agileProjectValue, entity.getId(), fields); return jiraService.getSingleAgileEntityIssue(agileProjectValue, issueKey); } - private Map getFieldsFromAgileEntity(AgileEntity entity) { + private Map getFieldsFromAgileEntity(AgileEntity entity, JIRAServiceProvider.JIRAService service) { Map fields = new HashMap<>(); - Iterator>> fieldsIterator = entity.getAllFields(); + Iterator> fieldsIterator = entity.getAllFields(); while (fieldsIterator.hasNext()) { - Map.Entry> field = fieldsIterator.next(); + Map.Entry field = fieldsIterator.next(); + + DataField dataField = field.getValue(); + + // As of PPM 9.50, data fields coming from PPM can be either simple string, simple User, or list of Users. + if (dataField == null) { + fields.put(field.getKey(), null); + } else if (DataField.DATA_TYPE.USER.equals(dataField.getType())) { + // This is one or more user; however, in JIRA the user fields usually only take one user - or at least that's how we'll synch them. + User user = null; + if (dataField.isList()) { + // PPM Multi user field + List users = (List)dataField.get(); + if (users != null && !users.isEmpty()) { + user = users.get(0); + } + } else { + // Single user + user = (User)dataField.get(); + } - StringBuilder value = new StringBuilder(""); + if (user == null) { + fields.put(field.getKey(), null); + } else { + // We need to retrieve the right Jira user matching the PPM user's email or username. + String jiraUsername = service.getJiraUsernameFromPpmUser(user); + fields.put(field.getKey(), jiraUsername == null ? null : JIRA_NAME_PREFIX + jiraUsername); + } - for (AgileEntityFieldValue v : field.getValue()) { - value.append(v.getValue()); + } else { + // we consider it a single String value. + fields.put(field.getKey(), dataField.get() == null ? null : dataField.get().toString()); } - - fields.put(field.getKey(), value.toString()); } return fields; @@ -114,10 +147,10 @@ private Map getFieldsFromAgileEntity(AgileEntity entity) { @Override public AgileEntity createEntity(String agileProjectValue, String entityType, AgileEntity entity, ValueSet instanceConfigurationParameters) { - Map fields = getFieldsFromAgileEntity(entity); - JIRAServiceProvider.JIRAService jiraService = service.get(instanceConfigurationParameters); + Map fields = getFieldsFromAgileEntity(entity, jiraService); + String issueKey = jiraService.createIssue(agileProjectValue, fields, null, entityType); return jiraService.getSingleAgileEntityIssue(agileProjectValue, issueKey); diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java index b6d9c33..f382857 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java @@ -8,6 +8,8 @@ import com.ppm.integration.agilesdk.connector.jira.rest.util.RestWrapper; import com.ppm.integration.agilesdk.connector.jira.rest.util.exception.RestRequestException; import com.ppm.integration.agilesdk.connector.jira.util.JiraIssuesRetrieverUrlBuilder; +import com.ppm.integration.agilesdk.dm.MultiUserField; +import com.ppm.integration.agilesdk.dm.StringField; import com.ppm.integration.agilesdk.epic.PortfolioEpicCreationInfo; import com.ppm.integration.agilesdk.model.AgileEntity; import com.ppm.integration.agilesdk.provider.Providers; @@ -182,7 +184,6 @@ public List getAllSprints(String projectKey) { logger.error("Error when trying to retrieve JIRA Sprint information for Board ID " + board.getId() + " ('" + board.getName() + "'). Sprints from this board will be ignored."); } - } return jiraSprints; @@ -247,9 +248,7 @@ public String createIssue(String projectKey, Map fields, String fieldsObj.put("project", project); fieldsObj.put("issuetype", issueType); - for (Map.Entry fieldEntry : fields.entrySet()) { - fieldsObj.put(fieldEntry.getKey(), fieldEntry.getValue()); - } + setJiraJSonFields(fieldsObj, fields, projectKey, issueTypeId); createIssuePayload.put("fields", fieldsObj); @@ -274,13 +273,13 @@ public String updateIssue(String projectKey, String issueKey, Map fieldEntry : fields.entrySet()) { - fieldsObj.put(fieldEntry.getKey(), fieldEntry.getValue()); - } + setJiraJSonFields(fieldsObj, fields, projectKey, issue.getTypeId()); updateIssuePayload.put("fields", fieldsObj); @@ -294,6 +293,38 @@ public String updateIssue(String projectKey, String issueKey, Map fields, String projectKey, + String issueTypeId) throws JSONException { + + // We need to know the types of the issue fields in order to parse them to number when needed. + Map fieldsInfo = getFields(projectKey, issueTypeId); + + + for (Map.Entry fieldEntry : fields.entrySet()) { + String fieldKey = fieldEntry.getKey(); + String value = fieldEntry.getValue(); + + JIRAFieldInfo fieldInfo = fieldsInfo.get(fieldKey); + + boolean isNumber = fieldInfo != null && "number".equals(fieldInfo.getType()); + + if (value == null || (isNumber && StringUtils.isBlank(value))) { + fieldsObj.put(fieldEntry.getKey(), (Object)null); + } else if (value.startsWith(JIRAConstants.JIRA_NAME_PREFIX)) { + value = value.substring(JIRAConstants.JIRA_NAME_PREFIX.length()); + JSONObject nameObj = new JSONObject(); + nameObj.put("name", value); + fieldsObj.put(fieldEntry.getKey(), nameObj); + } else { + if (isNumber) { + fieldsObj.put(fieldEntry.getKey(), Double.parseDouble(value)); + } else { + fieldsObj.put(fieldEntry.getKey(), value); + } + } + } + } + public String createEpic(String projectKey, PortfolioEpicCreationInfo epicInfo) { // Read all custom field info required to create an Epic initCustomFieldsInfo(); @@ -510,10 +541,51 @@ public List getAgileEntityIssuesModifiedSince(Set entityIds } private void addJSONObjectFieldToEntity(String fieldKey, JSONObject field, AgileEntity entity) throws JSONException { - String name = field.has("name") ? field.getString("name"):""; - // ID is Key attribute or ID if no key is present. - String key = field.has("key") ? field.getString("key"):(field.has("id") ? field.getString("id") : ""); - entity.addField(fieldKey, name, key); + + if (isUserField(field)) { + Long ppmUserId = getPpmUserIdFromJiraUserField(field); + if (ppmUserId == null) { + entity.addField(fieldKey, null); + } else { + // PPM Only supports Multi User fields for now + MultiUserField muf = new MultiUserField(); + com.ppm.integration.agilesdk.dm.User user = new com.ppm.integration.agilesdk.dm.User(); + user.setUserId(ppmUserId); + List users = new ArrayList<>(1); + users.add(user); + muf.set(users); + entity.addField(fieldKey, muf); + } + + } else { + // Standard field. + String name = field.has("name") ? field.getString("name") : ""; + StringField sf = new StringField(); + // Since only strings are supported, we only set the Name, not the key. That will be for when CodeMeaning will be supported. + sf.set(name); + entity.addField(fieldKey, sf); + } + } + + private Long getPpmUserIdFromJiraUserField(JSONObject field) throws JSONException { + String email = field.getString("emailAddress"); + + UserProvider provider = Providers.getUserProvider(JIRAIntegrationConnector.class); + User user = provider.getByEmail(email); + + if (user != null) { + return user.getUserId(); + } + + return null; + } + + private boolean isUserField(JSONObject field) throws JSONException { + return field != null && field.has("self") && field.has("emailAddress") && field.getString("self").contains("/user?"); + } + + private String getValueFromJsonObject(JSONObject jsonObject) throws JSONException { + return jsonObject.has("name") ? jsonObject.getString("name"):""; } /** @@ -637,7 +709,7 @@ private AgileEntity getAgileEntityFromIssueJSon(JSONObject issueObj) { if (fieldsObj.isNull(fieldKey)) { // Null fields in JIRA are considered empty fields in PPM. - entity.addField(fieldKey, "", ""); + entity.addField(fieldKey, new StringField()); continue; } @@ -648,19 +720,14 @@ private AgileEntity getAgileEntityFromIssueJSon(JSONObject issueObj) { addJSONObjectFieldToEntity(fieldKey, field, entity); } else if (fieldContents instanceof JSONArray) { - JSONArray field = (JSONArray)fieldContents; - for (int i = 0; i < field.length(); i++) { - Object arrayValue = field.get(i); - if (arrayValue instanceof JSONObject) { - addJSONObjectFieldToEntity(fieldKey, (JSONObject)arrayValue, entity); - } else if (arrayValue instanceof String) { - entity.addField(fieldKey, (String)arrayValue, (String)arrayValue); - } - // We don't support arrays in arrays. - } + StringField sf = getStringFieldFromJsonArray((JSONArray)fieldContents); + entity.addField(fieldKey, sf); + } else { // If it's not an object nor an array, it's a string - entity.addField(fieldKey, fieldContents.toString(), fieldContents.toString()); + StringField sf = new StringField(); + sf.set(fieldContents.toString()); + entity.addField(fieldKey, sf); } } @@ -679,8 +746,8 @@ private AgileEntity getAgileEntityFromIssueJSon(JSONObject issueObj) { } } - if (fieldsObj.has("key") && !fieldsObj.isNull("key")) { - entity.setId(fieldsObj.getString("key")); + if (issueObj.has("key") && !issueObj.isNull("key")) { + entity.setId(issueObj.getString("key")); } entity.setEntityUrl(getBaseUrl()+ "/browse/"+entity.getId()); @@ -693,6 +760,27 @@ private AgileEntity getAgileEntityFromIssueJSon(JSONObject issueObj) { } + private StringField getStringFieldFromJsonArray(JSONArray jsonArray) throws JSONException { + + + List values = new ArrayList<>(); + + for (int i = 0; i < jsonArray.length(); i++) { + Object arrayValue = jsonArray.get(i); + if (arrayValue instanceof JSONObject) { + values.add(getValueFromJsonObject((JSONObject)arrayValue)); + } else if (arrayValue instanceof String) { + values.add((String)arrayValue); + } + // We don't support arrays in arrays. + } + + StringField sf = new StringField(); + sf.set(StringUtils.join(values, ";")); + + return sf; + } + private List retrieveIssues(JiraIssuesRetrieverUrlBuilder searchUrlBuilder) { IssueRetrievalResult result = null; @@ -852,6 +940,8 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { // Common fields for all issues issue.setType(issueType); + issue.setTypeId(fields.getJSONObject("issuetype").getString("id")); + if (fields.has("status") && fields.getJSONObject("status").has("name")) { issue.setStatus(fields.getJSONObject("status").getString("name")); } @@ -1256,12 +1346,12 @@ private List pickWorklogs(List getFields(String projectKey, String issuetypeId) { + public Map getFields(String projectKey, String issuetypeId) { ClientResponse response = wrapper.sendGet(baseUri + JIRAConstants.CREATEMETA_SUFFIX + "?projectKeys="+projectKey+"&issuetypeIds="+issuetypeId+"&expand=projects.issuetypes.fields"); String jsonStr = response.getEntity(String.class); - List jiraFieldsInfo = new ArrayList<>(); + Map jiraFieldsInfo = new HashMap<>(); try { JSONObject result = new JSONObject(jsonStr); JSONObject projectInfo = result.getJSONArray("projects").getJSONObject(0); @@ -1270,7 +1360,7 @@ public List getFields(String projectKey, String issuetypeId) { for (String fieldKey : JSONObject.getNames(fields)) { JSONObject field = fields.getJSONObject(fieldKey); JIRAFieldInfo fieldInfo = JIRAFieldInfo.fromJSONObject(field, fieldKey); - jiraFieldsInfo.add(fieldInfo); + jiraFieldsInfo.put(fieldInfo.getKey(), fieldInfo); } } catch (JSONException e) { @@ -1326,5 +1416,41 @@ public String getAccountUsernameFromLogonUsername(String logonUsername) { return accountUsernameFromEmailMatch == null || hasMultipleEmailMatch ? logonUsername : accountUsernameFromEmailMatch; } + + public String getJiraUsernameFromPpmUser(com.ppm.integration.agilesdk.dm.User user) { + // We search first by email, or by username, or by full name, whatever comes first. + String jiraUserSearch = user.getEmail(); + if (StringUtils.isBlank(jiraUserSearch)) { + jiraUserSearch = user.getUsername(); + } + if (StringUtils.isBlank(jiraUserSearch)) { + jiraUserSearch = user.getFullName(); + } + if (StringUtils.isBlank(jiraUserSearch)) { + return null; + } + + ClientResponse response = + wrapper.sendGet(baseUri + JIRAConstants.SEARCH_USER + "?username=" + jiraUserSearch); + String jsonStr = response.getEntity(String.class); + + try { + JSONArray results = new JSONArray(jsonStr); + + if (results.length() == 0) { + return null; + } + + // We only match with the first user returned. + JSONObject userInfo = results.getJSONObject(0); + + return userInfo.getString("name"); + + } catch (JSONException e) { + logger.error("Error when retrieving User account info for PPM user " + user.getUserId(), e); + } + + return null; + } } } diff --git a/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAFieldInfo.java b/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAFieldInfo.java index edfce6b..0f67dc1 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAFieldInfo.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAFieldInfo.java @@ -1,8 +1,8 @@ package com.ppm.integration.agilesdk.connector.jira.model; -import com.ppm.integration.agilesdk.model.AgileEntityField; -import com.ppm.integration.agilesdk.model.AgileEntityFieldValue; +import com.ppm.integration.agilesdk.dm.DataField; +import com.ppm.integration.agilesdk.dm.StringField; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -24,7 +24,7 @@ public class JIRAFieldInfo { private String items; - private List allowedValues = null; + private List allowedValues = null; public static JIRAFieldInfo fromJSONObject(JSONObject obj, String key) { @@ -36,14 +36,12 @@ public static JIRAFieldInfo fromJSONObject(JSONObject obj, String key) { if (obj.has("allowedValues")) { JSONArray allowedValues = obj.getJSONArray("allowedValues"); if (allowedValues != null) { - fieldInfo.setAllowedValues(new ArrayList(allowedValues.length())); + fieldInfo.setAllowedValues(new ArrayList(allowedValues.length())); for (int i = 0 ; i < allowedValues.length() ; i++) { - AgileEntityField listValue = new AgileEntityField(); + DataField listValue = new StringField(); JSONObject listValueObj = allowedValues.getJSONObject(i); - listValue.setKey(listValueObj.getString("id")); - AgileEntityFieldValue value = new AgileEntityFieldValue(listValueObj.getString("name"), listValueObj.getString("id")); - listValue.setValue(value); + listValue.set(listValueObj.getString("name")); fieldInfo.getAllowedValues().add(listValue); } } @@ -121,11 +119,11 @@ public void setItems(String items) { this.items = items; } - public List getAllowedValues() { + public List getAllowedValues() { return allowedValues; } - public void setAllowedValues(List allowedValues) { + public void setAllowedValues(List allowedValues) { this.allowedValues = allowedValues; } } diff --git a/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAIssue.java b/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAIssue.java index f252147..1f63f8b 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAIssue.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/model/JIRAIssue.java @@ -12,6 +12,8 @@ public abstract class JIRAIssue extends JIRAEntity { private String type; + private String typeId; + private String authorName; private String creationDate; @@ -251,4 +253,12 @@ public void setResolutionDate(String resolutionDate) { public String getResolutionDate() { return resolutionDate; } + + public String getTypeId() { + return typeId; + } + + public void setTypeId(String typeId) { + this.typeId = typeId; + } }