diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java index e398319..40babea 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRAConstants.java @@ -136,4 +136,30 @@ public class JIRAConstants { public static final String KEY_ALL_EPICS = "all_epics"; public static final String KEY_VERSION = "version"; + // Timesheet import settings + + public static final String TS_ALL_PROJECTS = "ts_all_projects"; + public static final String TS_GROUP_WORK_BY = "ts_group_work_by"; + public static final String TS_GROUP_BY_PROJECT = "ts_group_by_project"; + public static final String TS_GROUP_BY_EPIC = "ts_group_by_epic"; + public static final String TS_GROUP_BY_ISSUE = "ts_group_by_issue"; + + public static final String TS_IMPORT_HOURS_OPTION = "ts_import_hours_option"; + public static final String TS_IMPORT_HOURS_HOURS_ONLY = "ts_import_hours_hours_only"; + public static final String TS_IMPORT_HOURS_SP_ONLY = "ts_import_hours_sp_only"; + public static final String TS_IMPORT_HOURS_BOTH = "ts_import_hours_both"; + + public static final String TS_SP_TO_HOURS_RATIO = "ts_sp_to_hours_ratio"; + + public static final String TS_ADJUST_HOURS = "ts_adjust_hours"; + public static final String TS_ADJUST_HOURS_YES = "ts_adjust_hours_yes"; + public static final String TS_ADJUST_HOURS_NO = "ts_adjust_hours_no"; + + public static final String TS_ADJUST_HOURS_CHOICE = "ts_adjust_hours_choice"; + public static final String TS_ADJUST_HOURS_CHOICE_DAILY = "ts_adjust_hours_choice_daily"; + public static final String TS_ADJUST_HOURS_CHOICE_TOTAL = "ts_adjust_hours_choice_total"; + + public static final String TS_DAILY_HOURS = "ts_daily_hours"; + + public static final String JIRA_FIELD_RESOLUTION_DATE = "resolutiondate"; } diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRAExternalWorkItem.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRAExternalWorkItem.java deleted file mode 100644 index a02b4ef..0000000 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRAExternalWorkItem.java +++ /dev/null @@ -1,64 +0,0 @@ - -package com.ppm.integration.agilesdk.connector.jira; - -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.HashMap; -import java.util.Map; - -import javax.xml.datatype.XMLGregorianCalendar; - -import com.ppm.integration.agilesdk.tm.ExternalWorkItem; -import com.ppm.integration.agilesdk.tm.ExternalWorkItemEffortBreakdown; - -public class JIRAExternalWorkItem extends ExternalWorkItem { - - private String name = ""; - - private Double totalEffort = 0.0; - - private String errorMessage = null; - - private Map timeSpentSeconds = new HashMap<>(); - - private XMLGregorianCalendar dateFrom; - - private XMLGregorianCalendar dateTo; - - public JIRAExternalWorkItem(String name, Double totalEffort, String errorMessage, XMLGregorianCalendar dateFrom, - XMLGregorianCalendar dateTo, Map timeSpentSeconds) { - this.name = name; - this.totalEffort = totalEffort; - this.errorMessage = errorMessage; - this.dateFrom = dateFrom; - this.dateTo = dateTo; - this.timeSpentSeconds = timeSpentSeconds; - } - - @Override - public String getName() { - return this.name; - } - - public Double getTotalEffort() { - return null; - } - - public ExternalWorkItemEffortBreakdown getEffortBreakDown() { - ExternalWorkItemEffortBreakdown eb = new ExternalWorkItemEffortBreakdown(); - Calendar cursor = dateFrom.toGregorianCalendar(); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - while (cursor.before(dateTo.toGregorianCalendar())) { - String cursorDate = dateFormat.format(cursor.getTime()); - if (timeSpentSeconds.containsKey(cursorDate)) { - eb.addEffort(cursorDate, timeSpentSeconds.get(cursorDate)); - } else { - eb.addEffort(cursorDate, 0); - } - cursor.add(Calendar.DAY_OF_MONTH, 1); - - } - return eb; - } - -} diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRAIntegrationConnector.properties b/src/com/ppm/integration/agilesdk/connector/jira/JIRAIntegrationConnector.properties index 6a97df4..cdc9851 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRAIntegrationConnector.properties +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRAIntegrationConnector.properties @@ -74,3 +74,32 @@ ACTUALS_SP = Convert Story Points to Effort ACTUALS_NO_ACTUALS = Do not sync Effort ACTUALS_SP_RATIO = Number of hours per Story Point: +TS_ALL_PROJECTS = All Projects + +TS_GROUP_WORK_BY = Group work into lines by: +TS_GROUP_BY_PROJECT = JIRA Project +TS_GROUP_BY_EPIC = Epic +TS_GROUP_BY_ISSUE = Issue + +TS_IMPORT_HOURS_OPTION = How to import hours: +TS_IMPORT_HOURS_HOURS_ONLY = Import logged work +TS_IMPORT_HOURS_SP_ONLY = Convert Done story points to hours by a ratio +TS_IMPORT_HOURS_BOTH = Use work if logged, otherwise use Done story points + +TS_SP_TO_HOURS_RATIO = Story Points to Hours ratio: + +TS_ADJUST_HOURS = Adjust to Ensure a Constant Daily Total? +TS_ADJUST_HOURS_NO = No, import time as it is +TS_ADJUST_HOURS_YES = Yes, adjust imported hours + +TS_ADJUST_HOURS_CHOICE = How to Adjust Imported Hours +TS_ADJUST_HOURS_CHOICE_DAILY = Keep the original ratio among work item's daily effort +TS_ADJUST_HOURS_CHOICE_TOTAL = Keep the original ratio among work item's total effort and distribute equal effort to each working day + +TS_DAILY_HOURS = Daily Total for Imported Hours: + +INTEGRATION_NUMBER_ONLY = Number only + +NO_EPIC_TIMESHEET_LINE_PREFIX = Issues without Epic for Project: + + diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java index 8867a87..98a2c38 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRAServiceProvider.java @@ -1,4 +1,3 @@ - package com.ppm.integration.agilesdk.connector.jira; import com.hp.ppm.user.model.User; @@ -12,8 +11,6 @@ import com.ppm.integration.agilesdk.epic.PortfolioEpicCreationInfo; import com.ppm.integration.agilesdk.provider.Providers; import com.ppm.integration.agilesdk.provider.UserProvider; -import com.ppm.integration.agilesdk.tm.ExternalWorkItem; -import com.ppm.integration.agilesdk.tm.ExternalWorkItemEffortBreakdown; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.apache.wink.client.ClientResponse; @@ -22,6 +19,7 @@ import org.json.JSONObject; import javax.xml.datatype.XMLGregorianCalendar; +import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -73,7 +71,6 @@ public JIRAServiceProvider useNonAdminAccount() { return this; } - public class JIRAService { private final Logger logger = Logger.getLogger(this.getClass()); @@ -104,7 +101,7 @@ public void setBaseUri(String baseUri) { // Base URI should not have a trailing slash. while (baseUri.endsWith("/")) { - baseUri = baseUri.substring(0, baseUri.length()-1); + baseUri = baseUri.substring(0, baseUri.length() - 1); } this.baseUri = baseUri; @@ -159,7 +156,8 @@ public List getAllSprints(String projectKey) { } catch (RestRequestException e) { // JIRA will sometimes throw an Error 500 when retrieving sprints // In this case, we'll simply ignore this sprint, come what may. - logger.error("Error when trying to retrieve JIRA Sprint information for Board ID "+board.getId()+" ('"+board.getName()+"'). Sprints from this board will be ignored."); + logger.error("Error when trying to retrieve JIRA Sprint information for Board ID " + board.getId() + + " ('" + board.getName() + "'). Sprints from this board will be ignored."); } } @@ -236,7 +234,7 @@ public String createEpic(String projectKey, PortfolioEpicCreationInfo epicInfo) JSONObject jsonObj = new JSONObject(jsonStr); return jsonObj.getString("key"); } catch (Exception e) { - throw new RuntimeException("Error while parsing Epic creation response from JIRA: "+jsonStr, e); + throw new RuntimeException("Error while parsing Epic creation response from JIRA: " + jsonStr, e); } } @@ -317,8 +315,6 @@ public List getAllIssues(String projectKey, String... issu return getAllIssues(projectKey, types); } - - public List getVersions(String projectKey) { List list = new ArrayList(); String query = @@ -340,9 +336,9 @@ public List getVersions(String projectKey) { version.setName(getStr(versionObj, "name")); version.setId(getStr(versionObj, "id")); version.setKey(getStr(versionObj, "id")); - version.setArchived(versionObj.has("archived")? versionObj.getBoolean("archived"):false); - version.setReleased(versionObj.has("released")? versionObj.getBoolean("released"):false); - version.setOverdue(versionObj.has("overdue")? versionObj.getBoolean("overdue"):false); + version.setArchived(versionObj.has("archived") ? versionObj.getBoolean("archived") : false); + version.setReleased(versionObj.has("released") ? versionObj.getBoolean("released") : false); + version.setOverdue(versionObj.has("overdue") ? versionObj.getBoolean("overdue") : false); version.setDescription(getStr(versionObj, "description")); version.setStartDate(getStr(versionObj, "startDate")); version.setReleaseDate(getStr(versionObj, "releasedDate")); @@ -357,11 +353,10 @@ public List getVersions(String projectKey) { } - - private long getAssigneeUserId(JSONObject fields) throws JSONException { - JSONObject assignee = (fields.has("assignee") &&!fields.isNull("assignee")) ? fields.getJSONObject("assignee") : null; + JSONObject assignee = + (fields.has("assignee") && !fields.isNull("assignee")) ? fields.getJSONObject("assignee") : null; if (assignee != null && assignee.has("emailAddress")) { String assigneeEmail = assignee.getString("emailAddress"); User ppmUser = userProvider.getByEmail(assigneeEmail); @@ -379,7 +374,7 @@ private List extractFixVersionIds(JSONObject fields) { try { JSONArray fixVersions = fields.getJSONArray("fixVersions"); - for (int i = 0 ; i < fixVersions.length() ; i++) { + for (int i = 0; i < fixVersions.length(); i++) { JSONObject fixVersion = fixVersions.getJSONObject(i); fixVersionsIds.add(fixVersion.getString("id")); } @@ -405,7 +400,9 @@ private String encodeForJQLQuery(String value) { return value; } - private JiraIssuesRetrieverUrlBuilder decorateOrderBySprintCreatedUrl(JiraIssuesRetrieverUrlBuilder urlBuilder) { + private JiraIssuesRetrieverUrlBuilder decorateOrderBySprintCreatedUrl( + JiraIssuesRetrieverUrlBuilder urlBuilder) + { return urlBuilder.setOrderBy(" sprint,created ASC "); } @@ -416,22 +413,51 @@ private JiraIssuesRetrieverUrlBuilder decorateOrderBySprintCreatedUrl(JiraIssues public JIRAIssue getSingleIssue(String projectKey, String issueKey) { JiraIssuesRetrieverUrlBuilder searchUrlBuilder = - new JiraIssuesRetrieverUrlBuilder(baseUri).setProjectKey(projectKey).setExpandLevel("schema").addAndConstraint("key="+issueKey) - .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, storyPointsCustomField); + new JiraIssuesRetrieverUrlBuilder(baseUri).setProjectKey(projectKey).setExpandLevel("schema") + .addAndConstraint("key=" + issueKey) + .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, + storyPointsCustomField); - IssueRetrievalResult result = runIssueRetrivalRequest(decorateOrderBySprintCreatedUrl(searchUrlBuilder).toUrlString()); + IssueRetrievalResult result = + runIssueRetrivalRequest(decorateOrderBySprintCreatedUrl(searchUrlBuilder).toUrlString()); if (result.getIssues().isEmpty()) { return null; } else if (result.getIssues().size() > 1) { - throw new RuntimeException("Retrieving issue "+issueKey+" in project "+projectKey+" returned more than 1 result ("+result.getIssues().size()+")"); + throw new RuntimeException( + "Retrieving issue " + issueKey + " in project " + projectKey + " returned more than 1 result (" + + result.getIssues().size() + ")"); } else { return result.getIssues().get(0); } } + /** + * We use the search issue API instead of the /rest/issue/{key} because we already have all + * the logic to get the right columns and build the right JIRAIssue in the search API. + */ + public List getIssues(String projectKey, Collection issueKeys) { + + if (issueKeys == null || issueKeys.isEmpty()) { + return new ArrayList<>(); + } + + JiraIssuesRetrieverUrlBuilder searchUrlBuilder = + new JiraIssuesRetrieverUrlBuilder(baseUri).setProjectKey(projectKey).setExpandLevel("schema") + .addAndConstraint("key in(" + StringUtils.join(issueKeys, ',')+")") + .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, + storyPointsCustomField); + + IssueRetrievalResult result = + runIssueRetrivalRequest(decorateOrderBySprintCreatedUrl(searchUrlBuilder).toUrlString()); + + + return result.getIssues(); + } + public List getAllBoards(String projectKey) { - ClientResponse response = wrapper.sendGet(baseUri + JIRAConstants.BOARD_SUFFIX + "?projectKeyOrId=" + projectKey); + ClientResponse response = + wrapper.sendGet(baseUri + JIRAConstants.BOARD_SUFFIX + "?projectKeyOrId=" + projectKey); List boards = new ArrayList(); @@ -446,7 +472,7 @@ public List getAllBoards(String projectKey) { JIRABoard board = new JIRABoard(); board.setId(jsonBoard.getString("id")); board.setKey(board.getId()); - board.setType(getStr(jsonBoard,"type")); + board.setType(getStr(jsonBoard, "type")); board.setName(jsonBoard.getString("name")); boards.add(board); } @@ -468,14 +494,13 @@ private String getStr(JSONObject obj, String key) { try { return obj.getString(key); } catch (JSONException e) { - throw new RuntimeException("Error when retrieving key "+key +" from JSon object "+obj.toString()); + throw new RuntimeException("Error when retrieving key " + key + " from JSon object " + obj.toString()); } } /** * This method returns all requested issue types, with sub-tasks always included and returned as part of their parent and never as standalone issues. *
Each Epic will have its content available directly as {@link JIRAEpic#getContents()}, but only for the issues types that were requested. - * */ public List getAllIssues(String projectKey, Set issueTypes) { @@ -490,10 +515,12 @@ public List getAllIssues(String projectKey, Set is return retrieveIssues(searchUrlBuilder); } + private List retrieveIssues(JiraIssuesRetrieverUrlBuilder searchUrlBuilder) { IssueRetrievalResult result = null; int fetchedResults = 0; + searchUrlBuilder.setStartAt(0); List allIssues = new ArrayList(); @@ -505,6 +532,33 @@ private List retrieveIssues(JiraIssuesRetrieverUrlBuilder } while (fetchedResults < result.getTotal()); + Map indexedIssues = new HashMap<>(); + + for (JIRAIssue issue : allIssues) { + indexedIssues.put(issue.getKey(), issue); + } + + // We first check if all parents of sub-tasks have been retrieved. If not, we retrieve them in a separate call. + List missingIssues = new ArrayList<>(); + for (JIRAIssue issue: allIssues) { + if (JIRAConstants.JIRA_ISSUE_SUB_TASK.equalsIgnoreCase(issue.getType())) { + JIRASubTask subTask = (JIRASubTask)issue; + JIRAIssue parent = indexedIssues.get(subTask.getParentKey()); + if (parent == null) { + missingIssues.add(subTask.getParentKey()); + } + } + } + + if (!missingIssues.isEmpty()) { + // We retrieved some sub-tasks but missed their parents. Let's retrieve them now. + List missingParents = getIssues(null, missingIssues); + for (JIRAIssue missingParent : missingParents) { + allIssues.add(missingParent); + } + } + + List processedIssues = new ArrayList(); Map subTaskableIssues = new HashMap(); @@ -521,7 +575,8 @@ private List retrieveIssues(JiraIssuesRetrieverUrlBuilder // Read all Stories/Tasks/Features, add them to Epic for (JIRAIssue issue : allIssues) { - if (!JIRAConstants.JIRA_ISSUE_EPIC.equalsIgnoreCase(issue.getType()) && issue instanceof JIRASubTaskableIssue) { + if (!JIRAConstants.JIRA_ISSUE_EPIC.equalsIgnoreCase(issue.getType()) + && issue instanceof JIRASubTaskableIssue) { processedIssues.add((JIRASubTaskableIssue)issue); subTaskableIssues.put(issue.getKey(), (JIRASubTaskableIssue)issue); @@ -562,7 +617,9 @@ private IssueRetrievalResult runIssueRetrivalRequest(String urlString) { try { JSONObject resultObj = new JSONObject(jsonStr); - IssueRetrievalResult result = new IssueRetrievalResult(resultObj.getInt("startAt"), resultObj.getInt("maxResults"), resultObj.getInt("total")); + IssueRetrievalResult result = + new IssueRetrievalResult(resultObj.getInt("startAt"), resultObj.getInt("maxResults"), + resultObj.getInt("total")); JSONArray issues = resultObj.getJSONArray("issues"); @@ -588,7 +645,7 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { JIRAIssue issue = null; - switch(issueType.toUpperCase()) { + switch (issueType.toUpperCase()) { case JIRAConstants.JIRA_ISSUE_EPIC: issue = new JIRAEpic(); break; @@ -612,7 +669,7 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { issue = new JIRABug(); break; default: - throw new RuntimeException("Unknow issue type:"+issueType); + throw new RuntimeException("Unknow issue type:" + issueType); } // Common fields for all issues @@ -639,12 +696,15 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { issue.setName(fields.getString(epicNameCustomField)); } issue.setAssigneePpmUserId(getAssigneeUserId(fields)); - issue.setAuthorName((fields.has("creator") && !fields.isNull("creator") && fields.getJSONObject("creator").has("displayName")) - ? fields.getJSONObject("creator").getString("displayName") : null); + issue.setAuthorName( + (fields.has("creator") && !fields.isNull("creator") && fields.getJSONObject("creator") + .has("displayName")) ? fields.getJSONObject("creator").getString("displayName") : null); issue.setCreationDate(fields.has("created") ? fields.getString("created") : ""); issue.setLastUpdateDate(fields.has("updated") ? fields.getString("updated") : ""); + issue.setResolutionDate(fields.has(JIRAConstants.JIRA_FIELD_RESOLUTION_DATE) ? fields.getString(JIRAConstants.JIRA_FIELD_RESOLUTION_DATE) : ""); issue.setEpicKey(fields.getString(epicLinkCustomField)); - issue.setStoryPoints((fields.has(storyPointsCustomField) && !fields.isNull(storyPointsCustomField)) ? new Double(fields.getDouble(storyPointsCustomField)).longValue() : null); + issue.setStoryPoints((fields.has(storyPointsCustomField) && !fields.isNull(storyPointsCustomField)) ? + new Double(fields.getDouble(storyPointsCustomField)).longValue() : null); issue.setFixVersionIds(extractFixVersionIds(fields)); @@ -652,14 +712,15 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { if (fields.has("timetracking") && !fields.isNull("timetracking")) { JSONObject timeTracking = fields.getJSONObject("timetracking"); if (timeTracking.has("remainingEstimateSeconds")) { - issue.getWork().setRemainingEstimateHours(JIRAIssueWork.secToHours(timeTracking.getInt("remainingEstimateSeconds"))); + issue.getWork().setRemainingEstimateHours( + JIRAIssueWork.secToHours(timeTracking.getInt("remainingEstimateSeconds"))); } if (timeTracking.has("timeSpentSeconds")) { - issue.getWork().setTimeSpentHours(JIRAIssueWork.secToHours(timeTracking.getInt("timeSpentSeconds"))); + issue.getWork() + .setTimeSpentHours(JIRAIssueWork.secToHours(timeTracking.getInt("timeSpentSeconds"))); } } - // Worklog info if (fields.has("worklog") && !fields.isNull("worklog")) { @@ -678,7 +739,8 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { if (worklogs != null) { for (int i = 0; i < worklogs.length(); i++) { JSONObject worklogEntry = worklogs.getJSONObject(i); - issue.getWork().addWorklogEntry(JIRAIssueWork.getWorklogEntryFromWorklogJSONObject(worklogEntry)); + issue.getWork() + .addWorklogEntry(JIRAIssueWork.getWorklogEntryFromWorklogJSONObject(worklogEntry)); } } } @@ -689,10 +751,12 @@ private JIRAIssue getIssueFromJSONObj(JSONObject obj) { } } - /** Retrieves Sprint ID from Sprint custom field. + /** + * Retrieves Sprint ID from Sprint custom field. * The example of origin format of sprintCustomfield is * "com.atlassian.greenhopper.service.sprint.Sprint@1f39706[id=1,rapidViewId=1,state=ACTIVE,name=SampleSprint - * 2,goal=,startDate=2016-12-07T06:18:24.224+08:00,endDate=2016-12-21T06:38:24.224+08:00,completeDate=,sequence=1]" + * 2,goal=,startDate=2016-12-07T06:18:24.224+08:00,endDate=2016-12-21T06:38:24.224+08:00,completeDate=,sequence=1]" + * * @param sprintCustomfields */ private String getSprintIdFromSprintCustomfield(JSONArray sprintCustomfields) throws JSONException { @@ -724,7 +788,8 @@ private String getSprintIdFromSprintCustomfield(JSONArray sprintCustomfields) th if ("state".equalsIgnoreCase(splited[0])) { if (splited.length == 2) { - isActiveOrFutureSprint = "ACTIVE".equalsIgnoreCase(splited[1]) || "FUTURE".equalsIgnoreCase(splited[1]); + isActiveOrFutureSprint = + "ACTIVE".equalsIgnoreCase(splited[1]) || "FUTURE".equalsIgnoreCase(splited[1]); } } } @@ -741,7 +806,8 @@ private String getSprintIdFromSprintCustomfield(JSONArray sprintCustomfields) th private JSONArray getWorklogsJSONArrayForIssue(String issueKey) { - ClientResponse response = wrapper.sendGet(baseUri + JIRAConstants.JIRA_GET_ISSUE_WORKLOG.replace("%issue%", issueKey)); + ClientResponse response = + wrapper.sendGet(baseUri + JIRAConstants.JIRA_GET_ISSUE_WORKLOG.replace("%issue%", issueKey)); String jsonStr = response.getEntity(String.class); try { @@ -749,9 +815,8 @@ private JSONArray getWorklogsJSONArrayForIssue(String issueKey) { JSONArray worklogsArray = obj.getJSONArray("worklogs"); return worklogsArray; - } catch (JSONException e) { - throw new RuntimeException("Ërror when retrieving all worklogs information for issue "+issueKey, e); + throw new RuntimeException("Ërror when retrieving all worklogs information for issue " + issueKey, e); } } @@ -761,9 +826,8 @@ public List getBoardIssues(String projectKey, Set JiraIssuesRetrieverUrlBuilder boardIssuesUrlBuilder = new JiraIssuesRetrieverUrlBuilder(baseUri).setProjectKey(projectKey) - .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, storyPointsCustomField) - .setBoardType(boardId) - .setIssuesTypes(issueTypes); + .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, + storyPointsCustomField).setBoardType(boardId).setIssuesTypes(issueTypes); return retrieveIssues(boardIssuesUrlBuilder); } @@ -773,18 +837,18 @@ public List getEpicIssues(String projectKey, Set i issueTypes.add(JIRAConstants.JIRA_ISSUE_SUB_TASK); JiraIssuesRetrieverUrlBuilder searchUrlBuilder = - new JiraIssuesRetrieverUrlBuilder(baseUri) - .setIssuesTypes(issueTypes).setProjectKey(projectKey) - .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, storyPointsCustomField); + new JiraIssuesRetrieverUrlBuilder(baseUri).setIssuesTypes(issueTypes).setProjectKey(projectKey) + .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, + storyPointsCustomField); // Retrieving only issues belonging to that epic searchUrlBuilder.addCustomFieldEqualsConstraint(epicLinkCustomField, epicKey); // We also want to retrieve the Epic itself - searchUrlBuilder.addOrConstraint("key="+epicKey); + searchUrlBuilder.addOrConstraint("key=" + epicKey); // We also want sub-tasks of the epic - searchUrlBuilder.addOrConstraint("parent="+epicKey); + searchUrlBuilder.addOrConstraint("parent=" + epicKey); return retrieveIssues(searchUrlBuilder); } @@ -795,20 +859,126 @@ public List getVersionIssues(String projectKey, Set"+modifiedAfterDate); + + if (!isBlank(projectKey)) { + spTimesheetUrlBuilder.setProjectKey(projectKey); + } + + List issues = retrieveIssues(spTimesheetUrlBuilder); + + Date fromDate = dateFrom.toGregorianCalendar().getTime(); + Date toDate = dateTo.toGregorianCalendar().getTime(); + + for (JIRASubTaskableIssue issue : issues) { + + Long sp = issue.getStoryPoints(); + if (sp == null || sp <= 0) { + // No SP = no effort + continue; + } + double issueEffort = sp * spToHoursRatio; + + // We check if + // - issue resolution date is within timesheet period. + // - status is Done (resolution date is defined). + Date resolutionDate = issue.getResolutionDateAsDate(); + if (resolutionDate == null) { + // Issue is not Done. + continue; + } + if (resolutionDate.after(toDate) || resolutionDate.before(fromDate)) { + // Issue was closed outside of timesheet window + continue; + } + + // We distribute issue effort on the (working) days between start of timesheet and completion period + + // We first compute the number of working between which to distribute the effort. + Date firstWorkingDay = tsDays[tsDays.length-1]; + int totalWorkDaysOfEffort = 0; + for (int i = 0 ; i < tsDays.length ; i++) { + + Date day = tsDays[i]; + + if (tsWorkDays[i]) { + // Working day + if (day.before(firstWorkingDay)) { + firstWorkingDay = day; + } + if (!day.after(resolutionDate)) { + ++totalWorkDaysOfEffort; + } + } + } + + if (totalWorkDaysOfEffort == 0) { + // there are no working days within scope, so we put all effort in the first working day, even if it's the last non-working day of the timesheet. + timesheetData.addIssueEffort(issue, sdf.format(firstWorkingDay), issueEffort); + } else { + double dailyEffort = issueEffort / totalWorkDaysOfEffort; + + // We distribute daily effort for each working day. + for (int i = 0 ; i < tsDays.length ; i++) { + Date day = tsDays[i]; + + if (tsWorkDays[i]) { + if (day.after(resolutionDate)) { + break; + } else { + timesheetData.addIssueEffort(issue, sdf.format(day), dailyEffort); + } + } + } + } + } + + return timesheetData; + } + + /** + * @return all the work items for worklogs logged by the passed author within the passed dates as timesheet lines. + */ + public JIRATimesheetData getWorkLogsTimesheetData(final XMLGregorianCalendar dateFrom, + final XMLGregorianCalendar dateTo, String projectKey, String author) { JiraIssuesRetrieverUrlBuilder worklogUrlBuilder = new JiraIssuesRetrieverUrlBuilder(baseUri) - .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, storyPointsCustomField) + .addExtraFields(epicLinkCustomField, epicNameCustomField, sprintIdCustomField, + storyPointsCustomField) .addAndConstraint("worklogDate>=" + dateFrom.toString().substring(0, 10)) .addAndConstraint("worklogDate<=" + dateTo.toString().substring(0, 10)) .addAndConstraint("worklogAuthor=" + encodeForJQLQuery(author)); @@ -819,7 +989,7 @@ public List getWorkItems(final XMLGregorianCalendar dateFrom, List issues = retrieveIssues(worklogUrlBuilder); - List timesheetLines = new ArrayList(); + JIRATimesheetData timesheetData = new JIRATimesheetData(); Date fromDate = dateFrom.toGregorianCalendar().getTime(); Date toDate = dateTo.toGregorianCalendar().getTime(); @@ -841,79 +1011,72 @@ public List getWorkItems(final XMLGregorianCalendar dateFrom, } if (!worklogs.isEmpty()) { - timesheetLines.add(new ExternalWorkItem() { - @Override public String getName() { - return "["+issue.getKey()+"] "+issue.getFullTaskName(); - } - @Override public ExternalWorkItemEffortBreakdown getEffortBreakDown() { - ExternalWorkItemEffortBreakdown effortBreakdown = new ExternalWorkItemEffortBreakdown(); - for (JIRAIssueWork.JIRAWorklogEntry worklog : worklogs) { - // We round time values to 2 decimals as we don't want values to look too ugly in Timesheet UI. - // Unfortunately as we're working with Doubles, this won't always work :( - // Luckily, the bad display will only happen on the import recap page, once the data gets in the timesheet page there's proper formatting. - effortBreakdown.addEffort(worklog.getDateStartedAsDate(), Math.round(worklog.getTimeSpentHours() * 100) / 100d); - } + for (JIRAIssueWork.JIRAWorklogEntry worklog : worklogs) { - insertZeroEffortDatesInBreakdown(effortBreakdown, dateFrom.toGregorianCalendar(), dateTo.toGregorianCalendar()); + timesheetData.addIssueEffort(issue, worklog.getDateStartedAsSimpleDate(), + Math.round(worklog.getTimeSpentHours() * 100) / 100d); + } - return effortBreakdown; - } - }); + insertZeroEffortDatesInBreakdown(timesheetData, issue, dateFrom.toGregorianCalendar(), + dateTo.toGregorianCalendar()); } } - return timesheetLines; + return timesheetData; } + // We must have some time info defined for every date of the timesheet, otherwise the timesheet UI in PPM will bug. + // This was fixed in PPM 9.42 as it will fill the gaps for you, but we want this connector to also work on PPM 9.41. + private void insertZeroEffortDatesInBreakdown(JIRATimesheetData data, JIRAIssue issue, Calendar startDate, + Calendar endDate) + { - } - - // We must have some time info defined for every date of the timesheet, otherwise the timesheet UI in PPM will bug. - // This was fixed in PPM 9.42 as it will fill the gaps for you, but we want this connector to also work on PPM 9.41. - private void insertZeroEffortDatesInBreakdown(ExternalWorkItemEffortBreakdown effortBreakdown, - Calendar startDate, Calendar endDate) { - - do { - effortBreakdown.addEffort(startDate.getTime(), 0d); - startDate.add(Calendar.DATE, 1); - } while (!startDate.after(endDate)); - - } - - /** - * Returns true if str is null, "", some spaces, or the string "null". - * @param str - * @return - */ - private boolean isBlank(String str) { - return StringUtils.isBlank(str) || "null".equalsIgnoreCase(str); - } + do { + data.addIssueEffort(issue, issue.convertDateToString(startDate.getTime()), 0d); + startDate.add(Calendar.DATE, 1); + } while (!startDate.after(endDate)); - /** - * @return the worklogs that match the constraints of date & author. - */ - private List pickWorklogs(List worklogs, Date fromDate, Date toDate, String author) - { - List validWorklogs = new ArrayList(); + } - if (worklogs == null || isBlank(author)) { - return validWorklogs; + /** + * Returns true if str is null, "", some spaces, or the string "null". + * + * @param str + * @return + */ + private boolean isBlank(String str) { + return StringUtils.isBlank(str) || "null".equalsIgnoreCase(str); } - for (JIRAIssueWork.JIRAWorklogEntry worklog : worklogs) { - if (worklog == null) { - continue; + /** + * @return the worklogs that match the constraints of date & author. + */ + private List pickWorklogs(List worklogs, + Date fromDate, Date toDate, String author) + { + List validWorklogs = new ArrayList(); + + if (worklogs == null || isBlank(author)) { + return validWorklogs; } - if (author.equalsIgnoreCase(worklog.getAuthorEmail()) || author.equalsIgnoreCase(worklog.getAuthorKey())) { - Date logDate = worklog.getDateStartedAsDate(); - if ((fromDate.before(logDate) && toDate.after(logDate)) || fromDate.equals(logDate) || toDate.equals(logDate)) { - validWorklogs.add(worklog); + for (JIRAIssueWork.JIRAWorklogEntry worklog : worklogs) { + if (worklog == null) { + continue; + } + + if (author.equalsIgnoreCase(worklog.getAuthorEmail()) || author + .equalsIgnoreCase(worklog.getAuthorKey())) { + Date logDate = worklog.getDateStartedAsDate(); + if ((fromDate.before(logDate) && toDate.after(logDate)) || fromDate.equals(logDate) || toDate + .equals(logDate)) { + validWorklogs.add(worklog); + } } } - } - return validWorklogs; + return validWorklogs; + } } } diff --git a/src/com/ppm/integration/agilesdk/connector/jira/JIRATimeSheetIntegration.java b/src/com/ppm/integration/agilesdk/connector/jira/JIRATimeSheetIntegration.java index 970fab7..5eacee2 100644 --- a/src/com/ppm/integration/agilesdk/connector/jira/JIRATimeSheetIntegration.java +++ b/src/com/ppm/integration/agilesdk/connector/jira/JIRATimeSheetIntegration.java @@ -2,12 +2,19 @@ package com.ppm.integration.agilesdk.connector.jira; import java.util.*; -import java.util.Map.Entry; import javax.xml.datatype.XMLGregorianCalendar; -import com.ppm.integration.agilesdk.connector.jira.model.JIRAIssueWork; -import com.ppm.integration.agilesdk.tm.ExternalWorkItemEffortBreakdown; +import com.ppm.integration.agilesdk.connector.jira.model.JIRAExternalWorkItem; +import com.ppm.integration.agilesdk.connector.jira.model.JIRAIssue; +import com.ppm.integration.agilesdk.connector.jira.model.JIRATimesheetData; +import com.ppm.integration.agilesdk.pm.LinkedTaskAgileEntityInfo; +import com.ppm.integration.agilesdk.provider.LocalizationProvider; +import com.ppm.integration.agilesdk.provider.Providers; +import com.ppm.integration.agilesdk.tm.*; +import com.ppm.integration.agilesdk.ui.*; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.apache.wink.client.ClientRuntimeException; @@ -15,14 +22,6 @@ import com.ppm.integration.agilesdk.connector.jira.model.JIRAProject; import com.ppm.integration.agilesdk.connector.jira.rest.util.exception.JIRAConnectivityExceptionHandler; import com.ppm.integration.agilesdk.connector.jira.rest.util.exception.RestRequestException; -import com.ppm.integration.agilesdk.tm.ExternalWorkItem; -import com.ppm.integration.agilesdk.tm.TimeSheetIntegration; -import com.ppm.integration.agilesdk.tm.TimeSheetIntegrationContext; -import com.ppm.integration.agilesdk.ui.DynamicDropdown; -import com.ppm.integration.agilesdk.ui.Field; -import com.ppm.integration.agilesdk.ui.LineBreaker; -import com.ppm.integration.agilesdk.ui.PasswordText; -import com.ppm.integration.agilesdk.ui.PlainText; public class JIRATimeSheetIntegration extends TimeSheetIntegration { @@ -33,21 +32,370 @@ public class JIRATimeSheetIntegration extends TimeSheetIntegration { @Override public List getExternalWorkItems(TimeSheetIntegrationContext timesheetContext, ValueSet values) { + // Retrieving external work items is done with admin account. + service.useAdminAccount(); + JIRAServiceProvider.JIRAService s = service.get(values); + XMLGregorianCalendar start = timesheetContext.currentTimeSheet().getPeriodStartDate(); XMLGregorianCalendar end = timesheetContext.currentTimeSheet().getPeriodEndDate(); String projectKey = values.get(JIRAConstants.KEY_JIRA_PROJECT); + if (JIRAConstants.TS_ALL_PROJECTS.equals(projectKey)) { + // Import all projects + projectKey = null; + } + String author = values.get(JIRAConstants.KEY_USERNAME); - List authorWorklogs = service.get(values).getWorkItems(start, end, projectKey, author); - return authorWorklogs; + String groupBy = values.get(JIRAConstants.TS_GROUP_WORK_BY); + String importEffortBy = values.get(JIRAConstants.TS_IMPORT_HOURS_OPTION); + String adjustHours = values.get(JIRAConstants.TS_ADJUST_HOURS); + String adjustHoursChoice = values.get(JIRAConstants.TS_ADJUST_HOURS_CHOICE); + + double spToHoursRatio = 8.0d; + try { + spToHoursRatio = Double.parseDouble(values.get(JIRAConstants.TS_SP_TO_HOURS_RATIO)); + } catch (Exception e) { + // keep default value; + } + + double dailyHours = 8.0d; + try { + dailyHours = Double.parseDouble(values.get(JIRAConstants.TS_DAILY_HOURS)); + } catch (Exception e) { + // keep default value; + } + + + Date[] tsDays = getTsDays(timesheetContext); + boolean[] tsWorkDays = getTsWorkDays(timesheetContext); + + JIRATimesheetData timesheetData = null; + + // First, we retrieve all timesheet lines with "raw" effort from Jira + if (JIRAConstants.TS_IMPORT_HOURS_SP_ONLY.equals(importEffortBy)) { + timesheetData = service.get(values).getSPTimesheetData(start, end, projectKey, author, spToHoursRatio, + tsDays, tsWorkDays); + } else if (JIRAConstants.TS_IMPORT_HOURS_BOTH.equals(importEffortBy)) { + timesheetData = s.getWorkLogsTimesheetData(start, end, projectKey, author); + mergeTimeSheetData(timesheetData, s.getSPTimesheetData(start, end, projectKey, author, spToHoursRatio, tsDays, tsWorkDays)); + } else { + // default = work logs only + timesheetData = s.getWorkLogsTimesheetData(start, end, projectKey, author); + } + + // Group by + List timesheetLines = convertTimesheetDataToTimesheetLines(timesheetData, groupBy, s); + + // Then we adjust all timesheet lines effort to match daily hours if needed + if (JIRAConstants.TS_ADJUST_HOURS_YES.equals(adjustHours)) { + if (JIRAConstants.TS_ADJUST_HOURS_CHOICE_DAILY.equals(adjustHoursChoice)) { + // Adjust day per day for any day with effort + adjustPerDayTo(timesheetLines, dailyHours); + + } else if (JIRAConstants.TS_ADJUST_HOURS_CHOICE_TOTAL.equals(adjustHoursChoice)) { + // Adjust totals & distribute over working days + adjustPerTotalAverageTo(timesheetLines, dailyHours, tsDays, tsWorkDays); + } + } + + List apiTimesheetLines = new ArrayList<>(); + apiTimesheetLines.addAll(timesheetLines); + return apiTimesheetLines; + } + + private void adjustPerTotalAverageTo(List lines, double dailyHours, Date[] tsDays, + boolean[] tsWorkDays) { + + int numWorkingDaysCount = 0; + for (boolean isWorkingDay: tsWorkDays) { + if (isWorkingDay) { + ++numWorkingDaysCount; + } + } + double targetTotalWork = numWorkingDaysCount * dailyHours; + + double totalWork = 0; + for (ExternalWorkItem wi : lines) { + totalWork += wi.getTotalEffort(); + } + + // Adjust work in each line + for (JIRAExternalWorkItem wi : lines) { + + double lineTotalEffort = wi.getTotalEffort(); + + // We clean up all effort in the line. + for (String day: new HashSet(wi.getEffortBreakDown().getEffortList().keySet())) { + wi.getEffortBreakDown().removeEffort(day); + } + + if (numWorkingDaysCount == 0 || totalWork == 0 || lineTotalEffort == 0 ) { + // We don't want a divide by zero + continue; + } + + // Insert new effort in each working day. + double lineWorkPerWorkDay = lineTotalEffort * (targetTotalWork / totalWork) / numWorkingDaysCount; + for (int i = 0 ; i < tsWorkDays.length ; i++) { + if (tsWorkDays[i]) { + wi.addEffort(tsDays[i], lineWorkPerWorkDay); + } + } + } + } + + private void adjustPerDayTo(List lines, double dailyHours) { + // First, we compute the total number of hours for each day + Map totalEffortPerDay = new HashMap<>(); + for (JIRAExternalWorkItem wi : lines) { + for (Map.Entry effort: wi.getEffortBreakDown().getEffortList().entrySet()) { + Double dayTotal = totalEffortPerDay.get(effort.getKey()); + if (dayTotal == null) { + dayTotal = 0d; + } + dayTotal += effort.getValue(); + totalEffortPerDay.put(effort.getKey(), dayTotal); + } + } + + // Adjusting effort for each days, line per line. + for (JIRAExternalWorkItem wi : lines) { + ExternalWorkItemEffortBreakdown efforts = wi.getEffortBreakDown(); + Map effortListCopy = new HashMap<>(efforts.getEffortList()); + for (Map.Entry effort : effortListCopy.entrySet()) { + Double dayTotal = totalEffortPerDay.get(effort.getKey()); + Double newDayEffort = effort.getValue() / dayTotal * dailyHours; + efforts.removeEffort(effort.getKey()); + wi.addEffort(effort.getKey(), newDayEffort); + } + } + } + + private List convertTimesheetDataToTimesheetLines(final JIRATimesheetData data, String groupBy, + JIRAServiceProvider.JIRAService s) + { + List timesheetLines = new ArrayList<>(); + + if (!data.hasData()) { + return timesheetLines; + } + + if (JIRAConstants.TS_GROUP_BY_PROJECT.equalsIgnoreCase(groupBy)) { + + // Group by Project + timesheetLines.addAll(getTimesheetLinesGroupedByProject(data.getIssuesInProjects(), data.getEffortPerIssue(), s, "")); + + } else if (JIRAConstants.TS_GROUP_BY_EPIC.equalsIgnoreCase(groupBy)) { + + // Group by Epic + // We must first make sure we have all the names of Epics that have effort against them + + Set epicsWithoutInfo = new HashSet<>(); + + for (String epicKey : data.getIssuesInEpics().keySet()) { + if (epicKey != null && !"null".equals(epicKey) && data.getIssues().get(epicKey) == null) { + epicsWithoutInfo.add(epicKey); + } + } + + // We now retrieve the Epics info from Jira and store them into data. + for (JIRAIssue epicWithoutInfo : s.getIssues(null, epicsWithoutInfo)) { + data.getIssues().put(epicWithoutInfo.getKey(), epicWithoutInfo); + } + + // One timesheet line per epic + for (Map.Entry> issuesInEpic : data.getIssuesInEpics().entrySet()) { + String epicKey = issuesInEpic.getKey(); + + if (epicKey == null || "null".equals(epicKey)) { + // These are all the issues without an Epic. We need to group them by projects and have one line per project. + Map> issuesPerProject = new HashMap<>(); + for (String issueKey : issuesInEpic.getValue()) { + JIRAIssue issue = data.getIssues().get(issueKey); + if (issue == null) { + // Shouldn't happen since we retrieve all issues + throw new RuntimeException("Could not find Jira Issue information for Issue "+issueKey); + } + Set issues = issuesPerProject.get(issue.getProjectKey()); + if (issues == null) { + issues = new HashSet<>(); + issuesPerProject.put(issue.getProjectKey(), issues); + } + issues.add(issueKey); + } + + // Create one line per project for issues without Epics + timesheetLines.addAll(getTimesheetLinesGroupedByProject(issuesPerProject, data.getEffortPerIssue(), s, Providers.getLocalizationProvider(JIRAIntegrationConnector.class).getConnectorText("NO_EPIC_TIMESHEET_LINE_PREFIX"))); + + continue; + } + + final Set epicIssuesKeys = issuesInEpic.getValue(); + + final JIRAIssue epic = data.getIssues().get(epicKey); + if (epic == null) { + // Shouldn't happen since we retrieve all issues + throw new RuntimeException("Could not find Jira Issue information for Epic "+epicKey); + } + + JIRAExternalWorkItem workItem = new JIRAExternalWorkItem(epic.getName(), epic.getProjectKey(), epic.getKey(), null); + ExternalWorkItemEffortBreakdown effort = workItem.getEffortBreakDown(); + + for (String issueKey : epicIssuesKeys) { + Map issueEfforts = data.getEffortPerIssue().get(issueKey); + if (issueEfforts == null) { + continue; + } + for (Map.Entry dailyEffort : issueEfforts.entrySet()) { + workItem.addEffort(dailyEffort.getKey(), dailyEffort.getValue()); + } + } + + timesheetLines.add(workItem); + } + } else { + // No group, one line per Issue + for (Map.Entry> effortEntry : data.getEffortPerIssue().entrySet()) { + String issueKey = effortEntry.getKey(); + final Map effortPerDay = effortEntry.getValue(); + final JIRAIssue issue = data.getIssues().get(issueKey); + + JIRAExternalWorkItem workItem = new JIRAExternalWorkItem(issue.getName(), issue.getProjectKey(), issue.getEpicKey(), issue.getKey()); + workItem.getLineAgileEntityInfo().setSprintId(issue.getSprintId()); + ExternalWorkItemEffortBreakdown effort = workItem.getEffortBreakDown(); + for (Map.Entry dailyEffort : effortPerDay.entrySet()) { + workItem.addEffort(dailyEffort.getKey(), dailyEffort.getValue()); + } + + timesheetLines.add(workItem); + } + } + + return timesheetLines; + } + + private List getTimesheetLinesGroupedByProject(final Map> issuesInProjects, + final Map> effortPerIssue, JIRAServiceProvider.JIRAService s, final String lineNamePrefix) + { + List workItems = new ArrayList<>(); + + // We need project names + List projects = s.getProjects(); + + // One timesheet line per project + for (Map.Entry> issuesInProject : issuesInProjects.entrySet()) { + final String projectKey = issuesInProject.getKey(); + final Set projectIssuesKeys = issuesInProject.getValue(); + + String projectName = "?"; + + for (JIRAProject proj : projects) { + if (projectKey.equals(proj.getKey())) { + projectName = proj.getName(); + break; + } + } + + final String lineName = projectName; + + JIRAExternalWorkItem workItem = new JIRAExternalWorkItem((StringUtils.isBlank(lineNamePrefix) ? lineName : lineNamePrefix + lineName), + projectKey, null, null); + ExternalWorkItemEffortBreakdown effort = workItem.getEffortBreakDown(); + + for (String issueKey : projectIssuesKeys) { + Map issueEfforts = effortPerIssue.get(issueKey); + if (issueEfforts == null) { + continue; + } + for (Map.Entry dailyEffort : issueEfforts.entrySet()) { + workItem.addEffort(dailyEffort.getKey(), dailyEffort.getValue()); + } + } + + workItems.add(workItem); + } + + return workItems; + } + + /** + * + * If a spData for an issue is not defined in the workLogsData for an issue ID, adds it. + * + */ + private void mergeTimeSheetData(JIRATimesheetData workLogsData, JIRATimesheetData spData) + { + Set spIssues = spData.getEffortPerIssue().keySet(); + + for (String spIssue : spIssues) { + if (!workLogsData.hasData(spIssue)) { + // Inserting sp effort into worklogEffort. + JIRAIssue issue = spData.getIssues().get(spIssue); + for (Map.Entry effort : spData.getEffortPerIssue().get(spIssue).entrySet()) { + workLogsData.addIssueEffort(issue, effort.getKey(), effort.getValue()); + } + } + } + } + + private boolean[] getTsWorkDays(TimeSheetIntegrationContext timesheetContext) { + try { + return timesheetContext.getTimesheetDaysWorkDay(); + } + catch (NoSuchMethodError e) { + // PPM 9.4X --> We consider Saturday & Sundays are not worked. + // We could get a better behavior by using PPM private APIs to retrieve Resource & Regional calendars, + // but let's not do that in an Agile SDK connector. Just upgrade to PPM 9.50+ ! + List workDays = new ArrayList(); + + Calendar fromDate = timesheetContext.currentTimeSheet().getPeriodStartDate().toGregorianCalendar(); + Calendar toDate = timesheetContext.currentTimeSheet().getPeriodEndDate().toGregorianCalendar(); + do { + int dayOfWeek = fromDate.get(Calendar.DAY_OF_WEEK); + if (dayOfWeek == Calendar.SATURDAY || dayOfWeek == Calendar.SUNDAY) { + workDays.add(Boolean.FALSE); + } else { + workDays.add(Boolean.TRUE); + } + + fromDate.add(Calendar.DATE, 1); + } while (!fromDate.after(toDate)); + + return ArrayUtils.toPrimitive(workDays.toArray(new Boolean[workDays.size()]), false); + + } + } + + private Date[] getTsDays(TimeSheetIntegrationContext timesheetContext) { + try { + return timesheetContext.getTimesheetDays(); + } + catch (NoSuchMethodError e) { + // PPM 9.4X + List days = new ArrayList(); + + Calendar fromDate = timesheetContext.currentTimeSheet().getPeriodStartDate().toGregorianCalendar(); + Calendar toDate = timesheetContext.currentTimeSheet().getPeriodEndDate().toGregorianCalendar(); + do { + days.add(fromDate.getTime()); + fromDate.add(Calendar.DATE, 1); + } while (!fromDate.after(toDate)); + + return days.toArray(new Date[days.size()]); + } } @Override public List getMappingConfigurationFields(ValueSet arg0) { + + final LocalizationProvider lp = Providers.getLocalizationProvider(JIRAIntegrationConnector.class); + + return Arrays.asList(new Field[] {new PlainText(JIRAConstants.KEY_USERNAME, "USERNAME", "", true), - new PasswordText(JIRAConstants.KEY_PASSWORD, "PASSWORD", "", true), new LineBreaker(), - new DynamicDropdown(JIRAConstants.KEY_JIRA_PROJECT, "JIRA_PROJECT", false) { + new PasswordText(JIRAConstants.KEY_PASSWORD, "PASSWORD", "", true), + new LineBreaker(), + new DynamicDropdown(JIRAConstants.KEY_JIRA_PROJECT, "JIRA_PROJECT", JIRAConstants.TS_ALL_PROJECTS, "", false) { @Override public List getDependencies() { @@ -69,15 +417,209 @@ public List