/* * This file is part of LibrePlan * * Copyright (C) 2013 St. Antoniusziekenhuis * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.importers; import static org.libreplan.web.I18nHelper._; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.joda.time.LocalDate; import org.libreplan.business.advance.bootstrap.PredefinedAdvancedTypes; import org.libreplan.business.advance.entities.AdvanceMeasurement; import org.libreplan.business.advance.entities.AdvanceType; import org.libreplan.business.advance.entities.DirectAdvanceAssignment; import org.libreplan.business.advance.exceptions.DuplicateAdvanceAssignmentForOrderElementException; import org.libreplan.business.advance.exceptions.DuplicateValueTrueReportGlobalAdvanceException; import org.libreplan.business.common.IAdHocTransactionService; import org.libreplan.business.common.IOnTransaction; import org.libreplan.business.common.daos.IConnectorDAO; import org.libreplan.business.common.entities.Connector; import org.libreplan.business.common.entities.ConnectorException; import org.libreplan.business.common.entities.PredefinedConnectorProperties; import org.libreplan.business.common.entities.PredefinedConnectors; import org.libreplan.business.orders.daos.IOrderSyncInfoDAO; import org.libreplan.business.orders.entities.HoursGroup; import org.libreplan.business.orders.entities.Order; import org.libreplan.business.orders.entities.OrderElement; import org.libreplan.business.orders.entities.OrderLine; import org.libreplan.business.orders.entities.OrderSyncInfo; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.importers.jira.IssueDTO; import org.libreplan.importers.jira.StatusDTO; import org.libreplan.importers.jira.TimeTrackingDTO; import org.libreplan.importers.jira.WorkLogDTO; import org.libreplan.importers.jira.WorkLogItemDTO; import org.libreplan.web.orders.IOrderModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; /** * Implementation of Synchronize order elements with jira issues * * @author Miciele Ghiorghis <m.ghiorghis@antoniusziekenhuis.nl> */ @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class JiraOrderElementSynchronizer implements IJiraOrderElementSynchronizer { private static final Log LOG = LogFactory .getLog(JiraOrderElementSynchronizer.class); private SynchronizationInfo synchronizationInfo; @Autowired private IConnectorDAO connectorDAO; @Autowired private IOrderSyncInfoDAO orderSyncInfoDAO; @Autowired private IAdHocTransactionService adHocTransactionService; @Autowired private IOrderModel orderModel; @Autowired private IJiraTimesheetSynchronizer jiraTimesheetSynchronizer; @Override @Transactional(readOnly = true) public List<String> getAllJiraLabels() throws ConnectorException { Connector connector = getJiraConnector(); if (connector == null) { throw new ConnectorException(_("JIRA connector not found")); } String jiraLabels = connector.getPropertiesAsMap().get( PredefinedConnectorProperties.JIRA_LABELS); String labels; try { new URL(jiraLabels); labels = JiraRESTClient.getAllLables(jiraLabels); } catch (MalformedURLException e) { labels = jiraLabels; } return Arrays.asList(StringUtils.split(labels, ",")); } @Override @Transactional(readOnly = true) public List<IssueDTO> getJiraIssues(String label) throws ConnectorException { Connector connector = getJiraConnector(); if (connector == null) { throw new ConnectorException(_("JIRA connector not found")); } if (!connector.areConnectionValuesValid()) { throw new ConnectorException( _("Connection values of JIRA connector are invalid")); } return getJiraIssues(label, connector); } /** * Gets all jira issues for the specified <code>label</code> * * @param label * the search criteria * @param connector * where to read the configuration parameters * @return a list of {@link IssueDTO} */ private List<IssueDTO> getJiraIssues(String label, Connector connector) { Map<String, String> properties = connector.getPropertiesAsMap(); String url = properties.get(PredefinedConnectorProperties.SERVER_URL); String username = properties .get(PredefinedConnectorProperties.USERNAME); String password = properties .get(PredefinedConnectorProperties.PASSWORD); String path = JiraRESTClient.PATH_SEARCH; String query = "labels=" + label; List<IssueDTO> issues = JiraRESTClient.getIssues(url, username, password, path, query); return issues; } @Override @Transactional(readOnly = true) public void syncOrderElementsWithJiraIssues(List<IssueDTO> issues, Order order) { synchronizationInfo = new SynchronizationInfo(_( "Synchronization order {0}", order.getName())); for (IssueDTO issue : issues) { String code = PredefinedConnectorProperties.JIRA_CODE_PREFIX + order.getCode() + "-" + issue.getKey(); String name = issue.getFields().getSummary(); OrderLine orderLine = syncOrderLine(order, code, name); if (orderLine == null) { synchronizationInfo.addFailedReason(_( "Order-element for \"{0}\" issue not found", issue.getKey())); continue; } EffortDuration loggedHours = getLoggedHours(issue.getFields() .getTimetracking()); EffortDuration estimatedHours = getEstimatedHours(issue.getFields() .getTimetracking(), loggedHours); if (estimatedHours.isZero()) { synchronizationInfo.addFailedReason(_( "Estimated time for \"{0}\" issue is 0", issue.getKey())); continue; } syncHoursGroup(orderLine, code, estimatedHours.getHours()); syncProgressMeasurement(orderLine, issue, estimatedHours, loggedHours); } } /** * Synchronize orderline * * check if orderLine is already exist for the given <code>order</code> If * it is, update <code>OrderLine.name</code> with the specified parameter * <code>name</code> (jira's name could be changed). If not, create new * {@link OrderLine} and add to {@link Order} * * @param order * an existing order * @param code * unique code for orderLine * @param name * name for the orderLine to be added or updated */ private OrderLine syncOrderLine(Order order, String code, String name) { OrderElement orderElement = order.getOrderElement(code); if (orderElement != null && !orderElement.isLeaf()) { return null; } OrderLine orderLine = (OrderLine) orderElement; if (orderLine == null) { orderLine = OrderLine.create(); orderLine.setCode(code); order.add(orderLine); } orderLine.setName(name); return orderLine; } /** * Synchronize hoursgroup * * Check if hoursGroup already exist for the given <code>orderLine</code>. * If it is, update <code>HoursGroup.workingHours</code> with the specified * parameter <code>workingHours</code>. If not, create new * {@link HoursGroup} and add to the {@link OrderLine} * * @param orderLine * an existing orderline * @param code * unique code for hoursgroup * @param workingHours * the working hours(jira's timetracking) */ private void syncHoursGroup(OrderLine orderLine, String code, Integer workingHours) { HoursGroup hoursGroup = orderLine.getHoursGroup(code); if (hoursGroup == null) { hoursGroup = HoursGroup.create(orderLine); hoursGroup.setCode(code); orderLine.addHoursGroup(hoursGroup); } hoursGroup.setWorkingHours(workingHours); } /** * Synchronize progress assignment and measurement * * @param orderLine * an exist orderLine * @param issue * jira's issue to synchronize with progress assignment and * measurement */ private void syncProgressMeasurement(OrderLine orderLine, IssueDTO issue, EffortDuration estimatedHours, EffortDuration loggedHours) { WorkLogDTO workLog = issue.getFields().getWorklog(); if (workLog == null) { synchronizationInfo.addFailedReason(_( "No worklogs found for \"{0}\" issue", issue.getKey())); return; } List<WorkLogItemDTO> workLogItems = workLog.getWorklogs(); if (workLogItems.isEmpty()) { synchronizationInfo.addFailedReason(_( "No worklog items found for \"{0}\" issue", issue.getKey())); return; } BigDecimal percentage; // if status is closed, the progress percentage is 100% regardless the // loggedHours and estimatedHours if (isIssueClosed(issue.getFields().getStatus())) { percentage = new BigDecimal(100); } else { percentage = loggedHours.dividedByAndResultAsBigDecimal( estimatedHours).multiply(new BigDecimal(100)); } LocalDate latestWorkLogDate = LocalDate .fromDateFields(getTheLatestWorkLoggedDate(workLogItems)); updateOrCreateProgressAssignmentAndMeasurement(orderLine, percentage, latestWorkLogDate); } /** * Get the estimated seconds from * {@link TimeTrackingDTO#getRemainingEstimateSeconds()} plus logged hours or * {@link TimeTrackingDTO#getOriginalEstimateSeconds()} and convert it to * {@link EffortDuration} * * @param timeTracking * where the estimated time to get from * @param loggedHours * hours already logged * @return estimatedHours */ private EffortDuration getEstimatedHours(TimeTrackingDTO timeTracking, EffortDuration loggedHours) { if (timeTracking == null) { return EffortDuration.zero(); } Integer timeestimate = timeTracking.getRemainingEstimateSeconds(); if (timeestimate != null && timeestimate > 0) { return EffortDuration.seconds(timeestimate).plus(loggedHours); } Integer timeoriginalestimate = timeTracking .getOriginalEstimateSeconds(); if (timeoriginalestimate != null) { return EffortDuration.seconds(timeoriginalestimate); } return EffortDuration.zero(); } /** * Get the time spent in seconds from * {@link TimeTrackingDTO#getTimeSpentSeconds()} and convert it to * {@link EffortDuration} * * @param timeTracking * where the timespent to get from * @return timespent in hous */ private EffortDuration getLoggedHours(TimeTrackingDTO timeTracking) { if (timeTracking == null) { return EffortDuration.zero(); } Integer timespentInSec = timeTracking.getTimeSpentSeconds(); if (timespentInSec != null && timespentInSec > 0) { return EffortDuration.seconds(timespentInSec); } return EffortDuration.zero(); } /** * updates {@link DirectAdvanceAssignment} and {@link AdvanceMeasurement} if * they already exist, otherwise create new one * * @param orderElement * an existing orderElement * @param percentage * percentage for advanced measurement * @param latestWorkLogDate * date for advanced measurement */ private void updateOrCreateProgressAssignmentAndMeasurement( OrderElement orderElement, BigDecimal percentage, LocalDate latestWorkLogDate) { AdvanceType advanceType = PredefinedAdvancedTypes.PERCENTAGE.getType(); DirectAdvanceAssignment directAdvanceAssignment = orderElement .getDirectAdvanceAssignmentByType(advanceType); if (directAdvanceAssignment == null) { directAdvanceAssignment = DirectAdvanceAssignment.create(false, new BigDecimal(100).setScale(2)); directAdvanceAssignment.setAdvanceType(advanceType); try { orderElement.addAdvanceAssignment(directAdvanceAssignment); } catch (DuplicateValueTrueReportGlobalAdvanceException e) { // This couldn't happen as it has just created the // directAdvanceAssignment with false as reportGlobalAdvance throw new RuntimeException(e); } catch (DuplicateAdvanceAssignmentForOrderElementException e) { // This could happen if a parent or child of the current // OrderElement has an advance of type PERCENTAGE synchronizationInfo .addFailedReason(_( "Duplicate value AdvanceAssignment for order element of \"{0}\"", orderElement.getCode())); return; } } AdvanceMeasurement advanceMeasurement = directAdvanceAssignment .getAdvanceMeasurementAtExactDate(latestWorkLogDate); if (advanceMeasurement == null) { advanceMeasurement = AdvanceMeasurement.create(); advanceMeasurement.setDate(latestWorkLogDate); directAdvanceAssignment.addAdvanceMeasurements(advanceMeasurement); } advanceMeasurement.setValue(percentage .setScale(2, RoundingMode.HALF_UP)); DirectAdvanceAssignment spreadAdvanceAssignment = orderElement .getReportGlobalAdvanceAssignment(); if (spreadAdvanceAssignment != null) { spreadAdvanceAssignment.setReportGlobalAdvance(false); } directAdvanceAssignment.setReportGlobalAdvance(true); } /** * check if issue is closed * * @param status * the status of the issue * @return true if status is Closed */ private boolean isIssueClosed(StatusDTO status) { if (status == null) { return false; } return status.getName().equals("Closed"); } /** * Loop through all <code>workLogItems</code> and get the latest date * * @param workLogItems * list of workLogItems * @return latest date */ private Date getTheLatestWorkLoggedDate(List<WorkLogItemDTO> workLogItems) { List<Date> dates = new ArrayList<Date>(); for (WorkLogItemDTO workLogItem : workLogItems) { if (workLogItem.getStarted() != null) { dates.add(workLogItem.getStarted()); } } return Collections.max(dates); } @Override public SynchronizationInfo getSynchronizationInfo() { return synchronizationInfo; } /** * returns JIRA connector */ private Connector getJiraConnector() { return connectorDAO.findUniqueByName(PredefinedConnectors.JIRA .getName()); } @Override @Transactional public void saveSyncInfo(final String key, final Order order) { adHocTransactionService .runOnAnotherTransaction(new IOnTransaction<Void>() { @Override public Void execute() { OrderSyncInfo orderSyncInfo = orderSyncInfoDAO .findByKeyOrderAndConnectorName(key, order, PredefinedConnectors.JIRA.getName()); if (orderSyncInfo == null) { orderSyncInfo = OrderSyncInfo.create(key, order, PredefinedConnectors.JIRA.getName()); } orderSyncInfo.setLastSyncDate(new Date()); orderSyncInfoDAO.save(orderSyncInfo); return null; } }); } @Override @Transactional(readOnly = true) public OrderSyncInfo getOrderLastSyncInfo(Order order) { return orderSyncInfoDAO.findLastSynchronizedInfoByOrderAndConnectorName( order, PredefinedConnectors.JIRA.getName()); } @Override @Transactional public List<SynchronizationInfo> syncOrderElementsWithJiraIssues() throws ConnectorException { Connector connector = getJiraConnector(); if (connector == null) { throw new ConnectorException(_("JIRA connector not found")); } if (!connector.areConnectionValuesValid()) { throw new ConnectorException( _("Connection values of JIRA connector are invalid")); } List<OrderSyncInfo> orderSyncInfos = orderSyncInfoDAO .findByConnectorName(PredefinedConnectors.JIRA.getName()); synchronizationInfo = new SynchronizationInfo(_("Synchronization")); List<SynchronizationInfo> syncInfos = new ArrayList<SynchronizationInfo>(); if (orderSyncInfos == null || orderSyncInfos.isEmpty()) { LOG.warn("No items found in 'OrderSyncInfo' to synchronize with JIRA issues"); synchronizationInfo .addFailedReason(_("No items found in 'OrderSyncInfo' to synchronize with JIRA issues")); syncInfos.add(synchronizationInfo); return syncInfos; } for (OrderSyncInfo orderSyncInfo : orderSyncInfos) { Order order = orderSyncInfo.getOrder(); LOG.info("Synchronizing '" + order.getName() + "'"); synchronizationInfo = new SynchronizationInfo(_( "Synchronization order {0}", order.getName())); List<IssueDTO> issueDTOs = getJiraIssues(orderSyncInfo.getKey(), connector); if (issueDTOs == null || issueDTOs.isEmpty()) { LOG.warn("No JIRA issues found for '" + orderSyncInfo.getKey() + "'"); synchronizationInfo.addFailedReason(_( "No JIRA issues found for key {0}", orderSyncInfo.getKey())); syncInfos.add(synchronizationInfo); continue; } orderModel.initEdit(order, null); syncOrderElementsWithJiraIssues(issueDTOs, order); if (!synchronizationInfo.isSuccessful()) { syncInfos.add(synchronizationInfo); continue; } orderModel.save(false); saveSyncInfo(orderSyncInfo.getKey(), order); jiraTimesheetSynchronizer.syncJiraTimesheetWithJiraIssues( issueDTOs, order); if (!synchronizationInfo.isSuccessful()) { syncInfos.add(synchronizationInfo); } } return syncInfos; } }