// Copyright © 2016 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.core.service; import fi.hsl.parkandride.core.back.ContactRepository; import fi.hsl.parkandride.core.back.FacilityRepository; import fi.hsl.parkandride.core.back.PredictionRepository; import fi.hsl.parkandride.core.back.UtilizationRepository; import fi.hsl.parkandride.core.domain.*; import org.joda.time.DateTime; import org.joda.time.Seconds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.function.Predicate; import static fi.hsl.parkandride.core.domain.Permission.*; import static fi.hsl.parkandride.core.service.AuthenticationService.authorize; public class FacilityService { private static final Logger logger = LoggerFactory.getLogger(FacilityService.class); private final FacilityRepository repository; private final UtilizationRepository utilizationRepository; private final ContactRepository contactRepository; private final ValidationService validationService; private final PredictionService predictionService; public FacilityService(FacilityRepository repository, UtilizationRepository utilizationRepository, ContactRepository contactRepository, ValidationService validationService, PredictionService predictionService) { this.repository = repository; this.utilizationRepository = utilizationRepository; this.contactRepository = contactRepository; this.validationService = validationService; this.predictionService = predictionService; } @TransactionalWrite public Facility createFacility(Facility facility, User currentUser) { authorize(currentUser, facility, FACILITY_CREATE); validate(facility); return getFacility(repository.insertFacility(facility)); } @TransactionalWrite public Facility updateFacility(long facilityId, Facility facility, User currentUser) { // User has update right to the input data... authorize(currentUser, facility, FACILITY_UPDATE); // ...and to the facility being updated Facility oldFacility = repository.getFacilityForUpdate(facilityId); authorize(currentUser, oldFacility, FACILITY_UPDATE); validate(facility); repository.updateFacility(facilityId, facility, oldFacility); return getFacility(facilityId); } private void validate(Facility facility) { Collection<Violation> violations = new ArrayList<>(); validationService.validate(facility, violations); CapacityPricingValidator.validateAndNormalize(facility, violations); validateContact(facility.operatorId, facility.contacts.emergency, "emergency", violations); validateContact(facility.operatorId, facility.contacts.operator, "operator", violations); validateContact(facility.operatorId, facility.contacts.service, "service", violations); if (!violations.isEmpty()) { throw new ValidationException(violations); } } private void validateContact(Long facilityOperatorId, Long contactId, String contactType, Collection<Violation> violations) { if (contactId != null) { Contact contact = contactRepository.getContact(contactId); if (contact == null) { violations.add(new Violation("NotFound", "contacts." + contactType, "contact not found")); } else if (contact.operatorId != null && !contact.operatorId.equals(facilityOperatorId)) { violations.add(new Violation("OperatorMismatch", "contacts." + contactType, "operator should match facility operator")); } } } @TransactionalRead public Facility getFacility(long id) { return repository.getFacility(id); } @TransactionalRead public SearchResults<FacilityInfo> search(PageableFacilitySearch search) { return repository.findFacilities(search); } @TransactionalRead public FacilitySummary summarize(FacilitySearch search) { return repository.summarizeFacilities(search); } @TransactionalWrite public void registerUtilization(long facilityId, List<Utilization> utilization, User currentUser) { FacilityInfo facility = repository.getFacilityInfo(facilityId); authorize(currentUser, facility, FACILITY_UTILIZATION_UPDATE); initUtilizationDefaults(facility, utilization); validateUtilizations(facilityId, utilization); autoUpdateFacilityCapacity(utilization); checkUtilizationApplicability(facility, utilization); utilizationRepository.insertUtilizations(utilization); predictionService.signalUpdateNeeded(utilization); } /** * Logs a warning for each utilization whose usage or capacity type is not included in the facility info * or whose number of spaces available exceeds the corresponding built capacity. */ private void checkUtilizationApplicability(FacilityInfo info, List<Utilization> utilization) { utilization.stream() .filter(u -> !info.usages.contains(u.usage) || info.builtCapacity.getOrDefault(u.capacityType, -1) < u.spacesAvailable ) .forEach(u -> logger.warn( "Unexpected utilization for facility id={}: usage {} not found in {} or spaces available ({}) exceeds built capacity ({})", info.id, u.usage, info.usages, u.spacesAvailable, info.builtCapacity.get(u.capacityType) )); } private static void initUtilizationDefaults(FacilityInfo facility, List<Utilization> utilization) { for (Utilization u : utilization) { if (u.facilityId == null) { u.facilityId = facility.id; } if (u.capacity == null) { u.capacity = facility.builtCapacity.getOrDefault(u.capacityType, u.spacesAvailable); } } } public void validateUtilizations(long facilityId, List<Utilization> utilizations) { for (int i = 0; i < utilizations.size(); i++) { List<Violation> violations = Violation.withPathPrefix("[" + i + "].", validateUtilization(utilizations.get(i), facilityId)); if (!violations.isEmpty()) { throw new ValidationException(violations); } } } private List<Violation> validateUtilization(Utilization u, long expectedFacilityId) { List<Violation> violations = new ArrayList<>(); validationService.validate(u, violations); if (!Objects.equals(u.facilityId, expectedFacilityId)) { violations.add(new Violation("NotEqual", "facilityId", "Expected to be " + expectedFacilityId + " but was " + u.facilityId)); } if (isFarIntoFuture(u.timestamp)) { violations.add(new Violation("NotFuture", "timestamp", u.timestamp + " is too far into future; the current time is " + DateTime.now())); } return violations; } private static boolean isFarIntoFuture(DateTime time) { Seconds gracePeriod = PredictionRepository.PREDICTION_RESOLUTION.toStandardSeconds().dividedBy(2); DateTime timeLimit = DateTime.now().plus(gracePeriod); return time != null && time.isAfter(timeLimit); } private void autoUpdateFacilityCapacity(List<Utilization> utilization) { for (Utilization u : utilization) { Facility facility = repository.getFacility(u.facilityId); Integer builtCapacity = facility.builtCapacity.get(u.capacityType); if (builtCapacity == null) { continue; } if (builtCapacity < u.capacity) { builtCapacity = u.capacity; facility.builtCapacity.put(u.capacityType, builtCapacity); repository.updateFacility(facility.id, facility); } Predicate<UnavailableCapacity> matchesUtilization = uc -> uc.capacityType.equals(u.capacityType) && uc.usage.equals(u.usage); int unavailableCapacity = facility.unavailableCapacities.stream() .filter(matchesUtilization) .map(uc -> uc.capacity) .findFirst() .orElse(0); if (builtCapacity - unavailableCapacity != u.capacity) { facility.unavailableCapacities.removeIf(matchesUtilization); facility.unavailableCapacities.add(new UnavailableCapacity(u.capacityType, u.usage, builtCapacity - u.capacity)); repository.updateFacility(facility.id, facility); } } } @TransactionalRead public Set<Utilization> findLatestUtilization(Long... facilityIds) { return utilizationRepository.findLatestUtilization(facilityIds); } }