package org.synyx.urlaubsverwaltung.web.application;
import org.joda.time.DateMidnight;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.synyx.urlaubsverwaltung.core.application.domain.Application;
import org.synyx.urlaubsverwaltung.core.application.domain.VacationCategory;
import org.synyx.urlaubsverwaltung.core.application.service.CalculationService;
import org.synyx.urlaubsverwaltung.core.overtime.OvertimeService;
import org.synyx.urlaubsverwaltung.core.period.DayLength;
import org.synyx.urlaubsverwaltung.core.settings.AbsenceSettings;
import org.synyx.urlaubsverwaltung.core.settings.Settings;
import org.synyx.urlaubsverwaltung.core.settings.SettingsService;
import org.synyx.urlaubsverwaltung.core.settings.WorkingTimeSettings;
import org.synyx.urlaubsverwaltung.core.util.CalcUtil;
import org.synyx.urlaubsverwaltung.core.workingtime.OverlapCase;
import org.synyx.urlaubsverwaltung.core.workingtime.OverlapService;
import org.synyx.urlaubsverwaltung.core.workingtime.WorkDaysService;
import org.synyx.urlaubsverwaltung.core.workingtime.WorkingTime;
import org.synyx.urlaubsverwaltung.core.workingtime.WorkingTimeService;
import java.math.BigDecimal;
import java.sql.Time;
import java.util.Optional;
/**
* This class validate if an {@link org.synyx.urlaubsverwaltung.web.application.ApplicationForLeaveForm} is filled
* correctly by the user, else it saves error messages in errors object.
*
* @author Aljona Murygina
*/
@Component
public class ApplicationValidator implements Validator {
private static final int MAX_CHARS = 200;
private static final String ERROR_MANDATORY_FIELD = "error.entry.mandatory";
private static final String ERROR_LENGTH = "error.entry.tooManyChars";
private static final String ERROR_PERIOD = "error.entry.invalidPeriod";
private static final String ERROR_HALF_DAY_PERIOD = "application.error.halfDayPeriod";
private static final String ERROR_MISSING_REASON = "application.error.missingReasonForSpecialLeave";
private static final String ERROR_PAST = "application.error.tooFarInThePast";
private static final String ERROR_TOO_LONG = "application.error.tooFarInTheFuture";
private static final String ERROR_ZERO_DAYS = "application.error.zeroDays";
private static final String ERROR_OVERLAP = "application.error.overlap";
private static final String ERROR_WORKING_TIME = "application.error.noValidWorkingTime";
private static final String ERROR_NOT_ENOUGH_DAYS = "application.error.notEnoughVacationDays";
private static final String ERROR_NOT_ENOUGH_OVERTIME = "application.error.notEnoughOvertime";
private static final String ERROR_MISSING_HOURS = "application.error.missingHoursForOvertime";
private static final String ERROR_INVALID_HOURS = "application.error.invalidHoursForOvertime";
private static final String ATTRIBUTE_START_DATE = "startDate";
private static final String ATTRIBUTE_END_DATE = "endDate";
private static final String ATTRIBUTE_REASON = "reason";
private static final String ATTRIBUTE_ADDRESS = "address";
private static final String ATTRIBUTE_COMMENT = "comment";
private static final String ATTRIBUTE_HOURS = "hours";
private final WorkingTimeService workingTimeService;
private final WorkDaysService calendarService;
private final OverlapService overlapService;
private final CalculationService calculationService;
private final SettingsService settingsService;
private final OvertimeService overtimeService;
@Autowired
public ApplicationValidator(WorkingTimeService workingTimeService, WorkDaysService calendarService,
OverlapService overlapService, CalculationService calculationService, SettingsService settingsService,
OvertimeService overtimeService) {
this.workingTimeService = workingTimeService;
this.calendarService = calendarService;
this.overlapService = overlapService;
this.calculationService = calculationService;
this.settingsService = settingsService;
this.overtimeService = overtimeService;
}
@Override
public boolean supports(Class<?> clazz) {
return ApplicationForLeaveForm.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ApplicationForLeaveForm applicationForm = (ApplicationForLeaveForm) target;
Settings settings = settingsService.getSettings();
// check if date fields are valid
validateDateFields(applicationForm, settings, errors);
// check hours
validateHours(applicationForm, settings, errors);
// check if reason is not filled
if (VacationCategory.SPECIALLEAVE.equals(applicationForm.getVacationType().getCategory())
&& !StringUtils.hasText(applicationForm.getReason())) {
errors.rejectValue(ATTRIBUTE_REASON, ERROR_MISSING_REASON);
}
// validate length of texts
validateStringLength(applicationForm.getReason(), ATTRIBUTE_REASON, errors);
validateStringLength(applicationForm.getAddress(), ATTRIBUTE_ADDRESS, errors);
validateStringLength(applicationForm.getComment(), ATTRIBUTE_COMMENT, errors);
if (!errors.hasErrors()) {
// validate if applying for leave is possible
// (check overlapping applications for leave, vacation days of the person etc.)
validateIfApplyingForLeaveIsPossible(applicationForm, settings, errors);
}
}
private void validateDateFields(ApplicationForLeaveForm applicationForLeave, Settings settings, Errors errors) {
DateMidnight startDate = applicationForLeave.getStartDate();
DateMidnight endDate = applicationForLeave.getEndDate();
validateNotNull(startDate, ATTRIBUTE_START_DATE, errors);
validateNotNull(endDate, ATTRIBUTE_END_DATE, errors);
if (startDate != null && endDate != null) {
validatePeriod(startDate, endDate, applicationForLeave.getDayLength(), settings, errors);
validateTime(applicationForLeave.getStartTime(), applicationForLeave.getEndTime(), errors);
}
}
private void validateNotNull(DateMidnight date, String field, Errors errors) {
// may be that date field is null because of cast exception, than there is already a field error
if (date == null && errors.getFieldErrors(field).isEmpty()) {
errors.rejectValue(field, ERROR_MANDATORY_FIELD);
}
}
private void validatePeriod(DateMidnight startDate, DateMidnight endDate, DayLength dayLength, Settings settings,
Errors errors) {
// ensure that startDate < endDate
if (startDate.isAfter(endDate)) {
errors.reject(ERROR_PERIOD);
} else {
AbsenceSettings absenceSettings = settings.getAbsenceSettings();
validateNotTooFarInTheFuture(endDate, absenceSettings, errors);
validateNotTooFarInThePast(startDate, absenceSettings, errors);
validateSameDayIfHalfDayPeriod(startDate, endDate, dayLength, errors);
}
}
private void validateNotTooFarInTheFuture(DateMidnight date, AbsenceSettings settings, Errors errors) {
Integer maximumMonths = settings.getMaximumMonthsToApplyForLeaveInAdvance();
DateMidnight future = DateMidnight.now().plusMonths(maximumMonths);
if (date.isAfter(future)) {
errors.reject(ERROR_TOO_LONG, new Object[] { settings.getMaximumMonthsToApplyForLeaveInAdvance() }, null);
}
}
private void validateNotTooFarInThePast(DateMidnight date, AbsenceSettings settings, Errors errors) {
Integer maximumMonths = settings.getMaximumMonthsToApplyForLeaveInAdvance();
DateMidnight past = DateMidnight.now().minusMonths(maximumMonths);
if (date.isBefore(past)) {
errors.reject(ERROR_PAST);
}
}
private void validateSameDayIfHalfDayPeriod(DateMidnight startDate, DateMidnight endDate, DayLength dayLength,
Errors errors) {
boolean isHalfDay = dayLength == DayLength.MORNING || dayLength == DayLength.NOON;
if (isHalfDay && !startDate.isEqual(endDate)) {
errors.reject(ERROR_HALF_DAY_PERIOD);
}
}
private void validateTime(Time startTime, Time endTime, Errors errors) {
boolean startTimeIsProvided = startTime != null;
boolean endTimeIsProvided = endTime != null;
boolean onlyStartTimeProvided = startTimeIsProvided && !endTimeIsProvided;
boolean onlyEndTimeProvided = !startTimeIsProvided && endTimeIsProvided;
if (onlyStartTimeProvided || onlyEndTimeProvided) {
errors.reject(ERROR_PERIOD);
}
if (startTimeIsProvided && endTimeIsProvided) {
long timeDifference = endTime.getTime() - startTime.getTime();
if (timeDifference <= 0) {
errors.reject(ERROR_PERIOD);
}
}
}
private void validateHours(ApplicationForLeaveForm applicationForLeave, Settings settings, Errors errors) {
BigDecimal hours = applicationForLeave.getHours();
boolean isOvertime = VacationCategory.OVERTIME.equals(applicationForLeave.getVacationType().getCategory());
boolean overtimeFunctionIsActive = settings.getWorkingTimeSettings().isOvertimeActive();
boolean hoursRequiredButNotProvided = isOvertime && overtimeFunctionIsActive && hours == null;
if (hoursRequiredButNotProvided && !errors.hasFieldErrors(ATTRIBUTE_HOURS)) {
errors.rejectValue(ATTRIBUTE_HOURS, ERROR_MISSING_HOURS);
}
if (hours != null && !CalcUtil.isPositive(hours)) {
errors.rejectValue(ATTRIBUTE_HOURS, ERROR_INVALID_HOURS);
}
}
private void validateStringLength(String text, String field, Errors errors) {
if (StringUtils.hasText(text) && text.length() > MAX_CHARS) {
errors.rejectValue(field, ERROR_LENGTH);
}
}
private void validateIfApplyingForLeaveIsPossible(ApplicationForLeaveForm applicationForm, Settings settings,
Errors errors) {
Application application = applicationForm.generateApplicationForLeave();
/**
* Ensure the person has a working time for the period of the application for leave
*/
if (!personHasWorkingTime(application)) {
errors.reject(ERROR_WORKING_TIME);
return;
}
/**
* Ensure that no one applies for leave for a vacation of 0 days
*/
if (vacationOfZeroDays(application)) {
errors.reject(ERROR_ZERO_DAYS);
return;
}
/**
* Ensure that there is no application for leave and no sick note in the same period
*/
if (vacationIsOverlapping(application)) {
errors.reject(ERROR_OVERLAP);
return;
}
/**
* Ensure that the person has enough vacation days left if the vacation type is
* {@link org.synyx.urlaubsverwaltung.core.application.domain.VacationCategory.HOLIDAY}
*/
if (!enoughVacationDaysLeft(application)) {
errors.reject(ERROR_NOT_ENOUGH_DAYS);
}
/**
* Ensure that the person has enough overtime hours left if the vacation type is
* {@link org.synyx.urlaubsverwaltung.core.application.domain.VacationCategory.OVERTIME}
*/
if (!enoughOvertimeHoursLeft(application, settings)) {
errors.reject(ERROR_NOT_ENOUGH_OVERTIME);
}
}
private boolean personHasWorkingTime(Application application) {
Optional<WorkingTime> workingTime = workingTimeService.getByPersonAndValidityDateEqualsOrMinorDate(
application.getPerson(), application.getStartDate());
return workingTime.isPresent();
}
private boolean vacationOfZeroDays(Application application) {
BigDecimal days = calendarService.getWorkDays(application.getDayLength(), application.getStartDate(),
application.getEndDate(), application.getPerson());
return CalcUtil.isZero(days);
}
private boolean vacationIsOverlapping(Application application) {
OverlapCase overlap = overlapService.checkOverlap(application);
return overlap == OverlapCase.FULLY_OVERLAPPING || overlap == OverlapCase.PARTLY_OVERLAPPING;
}
private boolean enoughVacationDaysLeft(Application application) {
boolean isHoliday = VacationCategory.HOLIDAY.equals(application.getVacationType().getCategory());
if (isHoliday) {
return calculationService.checkApplication(application);
}
return true;
}
private boolean enoughOvertimeHoursLeft(Application application, Settings settings) {
Boolean isOvertime = VacationCategory.OVERTIME.equals(application.getVacationType().getCategory());
if (isOvertime) {
WorkingTimeSettings workingTimeSettings = settings.getWorkingTimeSettings();
Boolean overtimeActive = workingTimeSettings.isOvertimeActive();
if (overtimeActive && application.getHours() != null) {
return checkOvertimeHours(application, workingTimeSettings);
}
}
return true;
}
private boolean checkOvertimeHours(Application application, WorkingTimeSettings settings) {
BigDecimal minimumOvertime = new BigDecimal(settings.getMinimumOvertime());
BigDecimal leftOvertimeForPerson = overtimeService.getLeftOvertimeForPerson(application.getPerson());
BigDecimal temporaryOvertimeForPerson = leftOvertimeForPerson.subtract(application.getHours());
return temporaryOvertimeForPerson.compareTo(minimumOvertime.negate()) >= 0;
}
}