/*
* 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.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.LocalDate;
import org.libreplan.business.calendars.daos.ICalendarExceptionTypeDAO;
import org.libreplan.business.calendars.entities.CalendarException;
import org.libreplan.business.calendars.entities.CalendarExceptionType;
import org.libreplan.business.calendars.entities.Capacity;
import org.libreplan.business.calendars.entities.PredefinedCalendarExceptionTypes;
import org.libreplan.business.calendars.entities.ResourceCalendar;
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.common.exceptions.InstanceNotFoundException;
import org.libreplan.business.resources.daos.IWorkerDAO;
import org.libreplan.business.resources.entities.Worker;
import org.libreplan.business.workingday.EffortDuration;
import org.libreplan.importers.RosterException.RosterExceptionItem;
import org.libreplan.importers.tim.DataDTO;
import org.libreplan.importers.tim.DepartmentDTO;
import org.libreplan.importers.tim.FilterDTO;
import org.libreplan.importers.tim.PeriodDTO;
import org.libreplan.importers.tim.PersonDTO;
import org.libreplan.importers.tim.RosterCategoryDTO;
import org.libreplan.importers.tim.RosterDTO;
import org.libreplan.importers.tim.RosterRequestDTO;
import org.libreplan.importers.tim.RosterResponseDTO;
import org.libreplan.web.calendars.IBaseCalendarModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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 import roosters from tim
*
* @author Miciele Ghiorghis <m.ghiorghis@antoniusziekenhuis.nl>
*/
@Component
@Scope(BeanDefinition.SCOPE_SINGLETON)
public class ImportRosterFromTim implements IImportRosterFromTim {
private static final Log LOG = LogFactory.getLog(ImportRosterFromTim.class);
@Autowired
private IWorkerDAO workerDAO;
@Autowired
private IConnectorDAO connectorDAO;
@Autowired
private IAdHocTransactionService adHocTransactionService;
@Autowired
private ICalendarExceptionTypeDAO calendarExceptionTypeDAO;
@Autowired
@Qualifier("subclass")
private IBaseCalendarModel baseCalendarModel;
private SynchronizationInfo synchronizationInfo;
/**
* Search criteria for roster exception days in RESPONSE message
* {@link RosterDTO}
*/
private static final String ABSENT = "Afwezig";
/**
* The word "Vakantie"(holiday) in RESPONSE message that would be translated
* to {@link PredefinedCalendarExceptionTypes#RESOURCE_HOLIDAY }
*/
private static final String HOLIDAY = "Vakantie";
/**
* The word "Feestdag"(bank holiday) in RESPONSE message that would be
* translated to {@link PredefinedCalendarExceptionTypes#BANK_HOLIDAY}
*/
private static final String BANK_HOLIDAY = "Feestdag";
@Override
@Transactional
public List<SynchronizationInfo> importRosters() throws ConnectorException {
Connector connector = connectorDAO
.findUniqueByName(PredefinedConnectors.TIM.getName());
if (connector == null) {
throw new ConnectorException(_("Tim connector not found"));
}
if (!connector.areConnectionValuesValid()) {
throw new ConnectorException(
_("Connection values of Tim connector are invalid"));
}
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);
int nrDaysRosterFromTim = Integer.parseInt(properties
.get(PredefinedConnectorProperties.TIM_NR_DAYS_ROSTER));
int productivityFactor = Integer.parseInt(properties
.get(PredefinedConnectorProperties.TIM_PRODUCTIVITY_FACTOR));
String departmentIds = properties
.get(PredefinedConnectorProperties.TIM_DEPARTAMENTS_IMPORT_ROSTER);
if (StringUtils.isBlank(departmentIds)) {
LOG.warn("No departments configured");
throw new ConnectorException(_("No departments configured"));
}
String[] departmentIdsArray = StringUtils.stripAll(StringUtils.split(
departmentIds, ","));
List<SynchronizationInfo> syncInfos = new ArrayList<SynchronizationInfo>();
for (String department : departmentIdsArray) {
LOG.info("Department: " + department);
synchronizationInfo = new SynchronizationInfo(_(
"Import roster for department {0}", department));
RosterRequestDTO rosterRequestDTO = createRosterRequest(department,
nrDaysRosterFromTim);
RosterResponseDTO rosterResponseDTO = TimSoapClient
.sendRequestReceiveResponse(url, userName, password,
rosterRequestDTO, RosterResponseDTO.class);
if (rosterResponseDTO != null) {
updateWorkersCalendarException(rosterResponseDTO,
productivityFactor);
if (!synchronizationInfo.isSuccessful()) {
syncInfos.add(synchronizationInfo);
}
} else {
LOG.error("No valid response for department " + department);
synchronizationInfo.addFailedReason(_(
"No valid response for department \"{0}\"",
department));
syncInfos.add(synchronizationInfo);
}
}
return syncInfos;
}
/**
* updates workers Exception calendar
*
* @param rosterResponse
* the response from Tim SOAP server
*/
private void updateWorkersCalendarException(
final RosterResponseDTO rosterResponse, final int productivityFactor) {
adHocTransactionService
.runOnAnotherTransaction(new IOnTransaction<Void>() {
@Override
public Void execute() {
List<RosterException> rosterExceptions = getRosterExceptions(
rosterResponse, productivityFactor);
if (!rosterExceptions.isEmpty()) {
updateCalendarException(rosterExceptions);
} else {
LOG.info("No roster-exceptions found in the response");
synchronizationInfo
.addFailedReason(_("No roster-exceptions found in the response"));
}
return null;
}
});
}
/**
* Loops through <code>rosterResponseDTO</code> and creates
* {@link RosterException}s and link them to the <code>worker</code>
*
* @param rosterResponseDTO
* the response
* @return a list of RosterExceptions
*/
private List<RosterException> getRosterExceptions(
RosterResponseDTO rosterResponseDTO, int productivityFactor) {
Map<String, List<RosterDTO>> map = getRosterExceptionPerWorker(rosterResponseDTO);
List<RosterException> rosterExceptions = new ArrayList<RosterException>();
for (Map.Entry<String, List<RosterDTO>> entry : map.entrySet()) {
Worker worker = null;
String workerCode = entry.getKey();
try {
worker = workerDAO.findUniqueByNif(workerCode);
} catch (InstanceNotFoundException e) {
LOG.warn("Worker '" + workerCode + "' not found");
synchronizationInfo.addFailedReason(_(
"Worker \"{0}\" not found",
workerCode));
}
if (worker != null) {
List<RosterDTO> list = entry.getValue();
Collections.sort(list, new Comparator<RosterDTO>() {
@Override
public int compare(RosterDTO o1, RosterDTO o2) {
return o1.getDate().compareTo(o2.getDate());
}
});
RosterException re = new RosterException(worker,
productivityFactor);
re.addRosterExceptions(list);
rosterExceptions.add(re);
}
}
return rosterExceptions;
}
/**
* Filters the roster on exceptions(absence) and creates a map with
* <code>personsNetwork-name</name> as key
* and list of <code>roster-exception</code> as value
*
* @param rosterResponseDTO
* the response
* @return person-roster exception map
*/
private Map<String, List<RosterDTO>> getRosterExceptionPerWorker(
RosterResponseDTO rosterResponseDTO) {
Map<String, List<RosterDTO>> rosterMap = new HashMap<String, List<RosterDTO>>();
List<RosterDTO> rosterDTOs = rosterResponseDTO.getRosters();
for (RosterDTO rosterDTO : rosterDTOs) {
if (rosterDTO.getPrecence().equals(ABSENT)) {
String personsNetWorkName = rosterDTO.getPersons().get(0)
.getNetworkName();
if (!rosterMap.containsKey(personsNetWorkName)) {
rosterMap.put(personsNetWorkName,
new ArrayList<RosterDTO>());
}
rosterMap.get(personsNetWorkName).add(rosterDTO);
}
}
return rosterMap;
}
/**
* updates the workers calendar exception
*
* @param rosterExceptions
* list of roster exceptions
*/
private void updateCalendarException(List<RosterException> rosterExceptions) {
for (RosterException rosterException : rosterExceptions) {
List<RosterExceptionItem> items = rosterException
.getRosterExceptionItems();
for (RosterExceptionItem item : items) {
updateCalendarExceptionPerWorker(rosterException.getWorker(),
item.getDate(), item.getExceptionType(),
item.getEffortDuration());
}
}
}
/**
* updates the calendar exception of the specified
* <code>{@link Worker}</code> for the specified <code>date</code>
*
* @param worker
* the worker
* @param date
* the date of the exception
* @param exceptionName
* the exception name
* @param effortDuration
* the exceptions effortDurtaion
*/
private void updateCalendarExceptionPerWorker(Worker worker,
LocalDate date, String exceptionName, EffortDuration effortDuration) {
CalendarExceptionType calendarExceptionType = getCalendarExceptionType(exceptionName);
if (calendarExceptionType == null) {
return;
}
ResourceCalendar resourceCalendar = (ResourceCalendar) worker
.getCalendarOrDefault();
CalendarException calendarExceptionDay = resourceCalendar
.getExceptionDay(date);
Capacity capacity = Capacity.create(effortDuration);
if (calendarExceptionDay != null) {
resourceCalendar.removeExceptionDay(calendarExceptionDay.getDate());
}
baseCalendarModel.initEdit(resourceCalendar);
baseCalendarModel.updateException(calendarExceptionType, date, date,
capacity);
baseCalendarModel.confirmSave();
}
/**
* Searches and returns the calendarExcptionType based on the specified
* <code>name</code>
*
* If the specified parameter <code>name</code> contains the word
* {@link ImportRosterFromTim#HOLIDAY}, the
* <code>calendarExceptionType</code> assumed to be the
* {@link PredefinedCalendarExceptionTypes#RESOURCE_HOLIDAY}, otherwise it
* searches in {@link CalendarExceptionType} for unique
* <code>calendarExceptionType</code>
*
* @param name
* the exception calendar name
*/
private CalendarExceptionType getCalendarExceptionType(String name) {
if (name == null || name.isEmpty()) {
LOG.error("Exception name should not be empty");
synchronizationInfo
.addFailedReason(_("Exception name should not be empty"));
return null;
}
try {
String nameToSearch = name;
if (nameToSearch.contains(HOLIDAY)) {
nameToSearch = PredefinedCalendarExceptionTypes.RESOURCE_HOLIDAY
.toString();
} else if (nameToSearch.equals(BANK_HOLIDAY)) {
nameToSearch = PredefinedCalendarExceptionTypes.BANK_HOLIDAY
.toString();
}
return calendarExceptionTypeDAO.findUniqueByName(nameToSearch);
} catch (InstanceNotFoundException e) {
LOG.error("Calendar exceptionType not found", e);
synchronizationInfo
.addFailedReason(_("Calendar exception day not found"));
}
return null;
}
/**
* creates and returns {@link RosterRequestDTO}
*
* @param nrDaysRosterFromTim
* nr of days required to set the end date
*/
private RosterRequestDTO createRosterRequest(String department,
int nrDaysRosterFromTim) {
RosterDTO rosterDTO = createRoster(nrDaysRosterFromTim);
PeriodDTO periodeDTO = new PeriodDTO();
periodeDTO.setStart(new org.joda.time.DateTime());
periodeDTO.setEnd(new org.joda.time.DateTime()
.plusDays(nrDaysRosterFromTim));
List<PeriodDTO> periodDTOs = new ArrayList<PeriodDTO>();
periodDTOs.add(periodeDTO);
DepartmentDTO departmentDTO = new DepartmentDTO();
departmentDTO.setRef(department);
FilterDTO filterDTO = new FilterDTO();
filterDTO.setPeriods(periodDTOs);
filterDTO.setDepartment(departmentDTO);
rosterDTO.setFilter(filterDTO);
rosterDTO.setPersons(createEmptyPerson());
rosterDTO.setRosterCategories(createEmptyRosterCategory());
rosterDTO.setDepartment(departmentDTO);
rosterDTO.setPrecence(new String());
rosterDTO.setPeriods(periodDTOs);
RosterRequestDTO exportRosterRequestDTO = new RosterRequestDTO();
DataDTO<RosterDTO> dataDTO = new DataDTO<RosterDTO>();
dataDTO.setData(rosterDTO);
exportRosterRequestDTO.setData(dataDTO);
return exportRosterRequestDTO;
}
/**
* creates and returns list of {@link PersonDTO} with empty
* {@link PersonDTO}
*
* This is an indication to Tim server that it should include this Person
* information in the RESPONSE message
*/
private List<PersonDTO> createEmptyPerson() {
List<PersonDTO> personDTOs = new ArrayList<PersonDTO>();
personDTOs.add(new PersonDTO());
return personDTOs;
}
/**
* creates and returns list of {@link RosterCategoryDTO} with empty
* {@link RosterCategoryDTO}
*
* This is an indication to Tim server that it should include this
* RosterCategory information in the RESPONSE message
*/
private List<RosterCategoryDTO> createEmptyRosterCategory() {
List<RosterCategoryDTO> rosterCategorieDTOs = new ArrayList<RosterCategoryDTO>();
RosterCategoryDTO rosterCategoryDTO = new RosterCategoryDTO();
rosterCategoryDTO.setName(new String());
rosterCategorieDTOs.add(rosterCategoryDTO);
return rosterCategorieDTOs;
}
/**
* creates and returns {@link RosterDTO}
*/
private RosterDTO createRoster(int nrDaysRosterFromTim) {
RosterDTO rosterDTO = new RosterDTO();
rosterDTO.setStartDate(new LocalDate());
rosterDTO.setEndDate(new LocalDate().plusDays(nrDaysRosterFromTim));
rosterDTO.setResourcePlanning(false);
rosterDTO.setDayPlanning(false);
rosterDTO.setCalendar(false);
rosterDTO.setNonPlaned(true);
rosterDTO.setFullDay(false);
rosterDTO.setConcept(false);
return rosterDTO;
}
@Override
public SynchronizationInfo getSynchronizationInfo() {
return synchronizationInfo;
}
}