/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.util;
import alfio.controller.form.UpdateTicketOwnerForm;
import alfio.controller.form.WaitingQueueSubscriptionForm;
import alfio.model.ContentLanguage;
import alfio.model.Event;
import alfio.model.TicketFieldConfiguration;
import alfio.model.modification.DateTimeModification;
import alfio.model.modification.EventModification;
import alfio.model.modification.TicketCategoryModification;
import alfio.model.result.ErrorCode;
import alfio.model.result.ValidationResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public final class Validator {
private static final Pattern SIMPLE_E_MAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9]+.*?@.+?\\..+$");
private Validator() {
}
public static ValidationResult validateEventHeader(Optional<Event> event, EventModification ev, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "shortName", "error.shortname");
if(ev.getOrganizationId() < 0) {
errors.rejectValue("organizationId", "error.organizationId");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "location", "error.location");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "websiteUrl", "error.websiteurl");
if(isInternal(event, ev)) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "description", "error.description");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "termsAndConditionsUrl", "error.termsandconditionsurl");
} else {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "externalUrl", "error.externalurl");
}
if(ev.getFileBlobId() == null) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "imageUrl", "error.imageurl");
if (!StringUtils.startsWith(ev.getImageUrl(), "https://")) {
errors.rejectValue("imageUrl", "error.imageurl");
}
}
return evaluateValidationResult(errors);
}
private static boolean isInternal(Optional<Event> event, EventModification ev) {
return event.map(Event::getType).orElse(ev.getEventType()) == Event.EventType.INTERNAL;
}
public static ValidationResult validateEventPrices(Optional<Event> event, EventModification ev, Errors errors) {
if(!isInternal(event, ev)) {
return ValidationResult.success();
}
if(!ev.isFreeOfCharge()) {
if(isCollectionEmpty(ev.getAllowedPaymentProxies())) {
errors.rejectValue("allowedPaymentProxies", "error.allowedpaymentproxies");
}
if(ev.getRegularPrice() == null || BigDecimal.ZERO.compareTo(ev.getRegularPrice()) >= 0) {
errors.rejectValue("regularPrice", "error.regularprice");
}
if(ev.getVatPercentage() == null || BigDecimal.ZERO.compareTo(ev.getVatPercentage()) > 0) {
errors.rejectValue("vat", "error.vat");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "currency", "error.currency");
}
if(ev.getAvailableSeats() < 1) {
errors.rejectValue("availableSeats", "error.availableseats");
}
return evaluateValidationResult(errors);
}
public static ValidationResult validateCategory(TicketCategoryModification category, Errors errors, String prefix) {
if(StringUtils.isBlank(category.getName())) {
errors.rejectValue(prefix + "name", "error.category.name");
}
if(category.isBounded() && category.getMaxTickets() < 1) {
errors.rejectValue(prefix + "maxTickets", "error.category.maxtickets");
}
if(!category.getInception().isBefore(category.getExpiration())) {
errors.rejectValue(prefix + "dateString", "error.date");
}
return evaluateValidationResult(errors);
}
public static ValidationResult validateCategory(TicketCategoryModification category, Errors errors) {
return validateCategory(category, errors, "");
}
private static boolean isCollectionEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
public static ValidationResult evaluateValidationResult(Errors errors) {
if (errors.hasFieldErrors()) {
return ValidationResult.failed(errors.getFieldErrors()
.stream().map(ValidationResult.ErrorDescriptor::fromFieldError)
.collect(Collectors.toList()));
}
return ValidationResult.success();
}
public static ValidationResult validateTicketAssignment(UpdateTicketOwnerForm form, List<TicketFieldConfiguration> additionalFieldsForEvent, Optional<Errors> errorsOptional, Event event) {
if(!errorsOptional.isPresent()) {
return ValidationResult.success();//already validated
}
Errors errors = errorsOptional.get();
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "error.email");
String email = form.getEmail();
if(!isEmailValid(email)) {
errors.rejectValue("email", "error.email");
}
if(event.mustUseFirstAndLastName()) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", ErrorsCode.STEP_2_EMPTY_FIRSTNAME);
validateMaxLength(form.getFirstName(), "firstName", ErrorsCode.STEP_2_MAX_LENGTH_FIRSTNAME, 255, errors);
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", ErrorsCode.STEP_2_EMPTY_LASTNAME);
validateMaxLength(form.getLastName(), "lastName", ErrorsCode.STEP_2_MAX_LENGTH_LASTNAME, 255, errors);
} else {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "fullName", ErrorsCode.STEP_2_EMPTY_FULLNAME);
validateMaxLength(form.getFullName(), "fullName", ErrorsCode.STEP_2_MAX_LENGTH_FULLNAME, 255, errors);
}
//
for(TicketFieldConfiguration fieldConf : additionalFieldsForEvent) {
boolean isField = form.getAdditional() !=null && form.getAdditional().containsKey(fieldConf.getName());
if(!isField) {
continue;
}
form.getAdditional().get(fieldConf.getName()).forEach(formValue -> {
if(fieldConf.isMaxLengthDefined()) {
validateMaxLength(formValue, "additional['"+fieldConf.getName()+"']", "error."+fieldConf.getName(), fieldConf.getMaxLength(), errors);
}
if(!fieldConf.getRestrictedValues().isEmpty()) {
validateRestrictedValue(formValue, "additional['"+fieldConf.getName()+"']", "error."+fieldConf.getName(), fieldConf.getRestrictedValues(), errors);
}
if(fieldConf.isRequired() && StringUtils.isBlank(formValue)){
errors.rejectValue("additional['"+fieldConf.getName()+"']", "error."+fieldConf.getName());
}
});
//TODO: complete checks: min length
}
return evaluateValidationResult(errors);
}
private static void validateRestrictedValue(String value, String fieldName, String errorCode, List<String> restrictedValues, Errors errors) {
if(StringUtils.isNotBlank(value) && !restrictedValues.contains(value)) {
errors.rejectValue(fieldName, errorCode);
}
}
private static boolean isEmailValid(String email) {
return StringUtils.isNotEmpty(email) && SIMPLE_E_MAIL_PATTERN.matcher(email).matches();
}
public static void validateMaxLength(String value, String fieldName, String errorCode, int maxLength, Errors errors) {
if(StringUtils.isNotBlank(value) && StringUtils.length(value) > maxLength) {
errors.rejectValue(fieldName, errorCode);
}
}
public static ValidationResult validateWaitingQueueSubscription(WaitingQueueSubscriptionForm form, Errors errors, Event event) {
if(!form.isTermAndConditionsAccepted()) {
errors.rejectValue("termAndConditionsAccepted", "error.termAndConditionsAccepted");
}
if(event.mustUseFirstAndLastName()) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "error.firstname");
validateMaxLength(form.getFirstName(), "firstName", "error.firstname", 255, errors);
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "lastname.fullname");
validateMaxLength(form.getLastName(), "lastName", "error.lastname", 255, errors);
} else {
validateMaxLength(form.getFullName(), "fullName", "error.fullname", 255, errors);
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "fullName", "error.fullname");
}
if(!isEmailValid(form.getEmail())) {
errors.rejectValue("email", "error.email");
}
if(form.getUserLanguage() == null) {
errors.rejectValue("userLanguage", "error.userLanguage");
}
return evaluateValidationResult(errors);
}
public static ValidationResult validateAdditionalService(EventModification.AdditionalService additionalService, Errors errors) {
return validateAdditionalService(additionalService, null, errors);
}
public static ValidationResult validateAdditionalService(EventModification.AdditionalService additionalService, EventModification eventModification, Errors errors) {
if(additionalService.isFixPrice() && !Optional.ofNullable(additionalService.getPrice()).filter(p -> p.compareTo(BigDecimal.ZERO) >= 0).isPresent()) {
errors.rejectValue("additionalServices", "error.price");
}
List<EventModification.AdditionalServiceText> descriptions = additionalService.getDescription();
List<EventModification.AdditionalServiceText> titles = additionalService.getTitle();
if(descriptions == null || titles == null || titles.size() != descriptions.size()) {
errors.rejectValue("additionalServices", "error.title");
errors.rejectValue("additionalServices", "error.description");
} else {
if(!validateDescriptionList(titles) || !containsAllRequiredTranslations(eventModification, titles)) {
errors.rejectValue("additionalServices", "error.title");
}
if(!validateDescriptionList(descriptions) || !containsAllRequiredTranslations(eventModification, descriptions)) {
errors.rejectValue("additionalServices", "error.description");
}
}
DateTimeModification inception = additionalService.getInception();
DateTimeModification expiration = additionalService.getExpiration();
if(inception == null || expiration == null || expiration.isBefore(inception)) {
errors.rejectValue("additionalServices", "error.inception");
errors.rejectValue("additionalServices", "error.expiration");
} else if(eventModification != null && expiration.isAfter(eventModification.getEnd())) {
errors.rejectValue("additionalServices", "error.expiration");
}
return evaluateValidationResult(errors);
}
private static boolean containsAllRequiredTranslations(EventModification eventModification, List<EventModification.AdditionalServiceText> descriptions) {
Optional<EventModification> optional = Optional.ofNullable(eventModification);
return !optional.isPresent() ||
optional.map(e -> ContentLanguage.findAllFor(e.getLocales()))
.filter(l -> l.stream().allMatch(l1 -> descriptions.stream().anyMatch(d -> d.getLocale().equals(l1.getLanguage()))))
.isPresent();
}
private static boolean validateDescriptionList(List<EventModification.AdditionalServiceText> descriptions) {
return descriptions.stream().allMatch(t -> StringUtils.isNotBlank(t.getValue()));
}
public static ValidationResult validateAdditionalFields(List<TicketFieldConfiguration> fieldConf, EventModification.AdditionalField field, Errors errors){
String duplicateName = fieldConf.stream().filter(f->f.getName().equalsIgnoreCase(field.getName())).map(TicketFieldConfiguration::getName).findAny().orElse("");
if(StringUtils.isNotBlank(duplicateName)){
errors.rejectValue("name", ErrorCode.DUPLICATE);
}
return evaluateValidationResult(errors);
}
}