// Copyright © 2016 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.dev; import com.google.common.collect.Lists; import fi.hsl.parkandride.FeatureProfile; import fi.hsl.parkandride.back.ContactDao; import fi.hsl.parkandride.back.FacilityDao; import fi.hsl.parkandride.back.HubDao; import fi.hsl.parkandride.back.OperatorDao; import fi.hsl.parkandride.core.back.*; import fi.hsl.parkandride.core.domain.*; import fi.hsl.parkandride.core.service.*; import org.joda.time.DateTime; import org.joda.time.Minutes; import org.joda.time.ReadablePeriod; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.validation.constraints.NotNull; import java.util.*; import java.util.stream.Stream; import java.util.stream.StreamSupport; import static fi.hsl.parkandride.front.UrlSchema.*; import static java.util.Spliterators.spliteratorUnknownSize; import static java.util.stream.Collectors.toList; import static org.springframework.http.HttpStatus.*; import static org.springframework.web.bind.annotation.RequestMethod.*; @RestController @Profile({ FeatureProfile.DEV_API}) public class DevController { @Resource ContactService contactService; @Resource FacilityRepository facilityRepository; @Resource HubRepository hubRepository; @Resource ContactRepository contactRepository; @Resource OperatorRepository operatorRepository; @Resource DevHelper devHelper; @Resource UserService userService; @Resource AuthenticationService authenticationService; @Resource UserRepository userRepository; @Resource PredictionService predictionService; @Resource FacilityService facilityService; @Resource UtilizationRepository utilizationRepository; @RequestMapping(method = POST, value = DEV_LOGIN) public ResponseEntity<Login> login(@RequestBody NewUser newUser) { User user = devHelper.createOrUpdateUser(newUser); Login login = devHelper.login(user.username); return new ResponseEntity<>(login, OK); } @RequestMapping(method = DELETE, value = DEV_FACILITIES) @TransactionalWrite public ResponseEntity<Void> deleteFacilities() { devHelper.deleteFacilities(); return new ResponseEntity<>(OK); } @RequestMapping(method = DELETE, value = DEV_HUBS) @TransactionalWrite public ResponseEntity<Void> deleteHubs() { devHelper.deleteHubs(); return new ResponseEntity<>(OK); } @RequestMapping(method = DELETE, value = DEV_CONTACTS) @TransactionalWrite public ResponseEntity<Void> deleteContacts() { devHelper.deleteContacts(); return new ResponseEntity<>(OK); } @RequestMapping(method = DELETE, value = DEV_OPERATORS) @TransactionalWrite public ResponseEntity<Void> deleteOperators() { devHelper.deleteOperators(); return new ResponseEntity<>(OK); } @RequestMapping(method = DELETE, value = DEV_USERS) @TransactionalWrite public ResponseEntity<Void> deleteUsers() { devHelper.deleteUsers(); return new ResponseEntity<>(OK); } @RequestMapping(method = PUT, value = DEV_USERS) @TransactionalWrite public ResponseEntity<List<User>> pushUsers(@RequestBody List<NewUser> newUsers) { List<User> users = new ArrayList<>(newUsers.size()); for (NewUser newUser : newUsers) { users.add(devHelper.createOrUpdateUser(newUser)); } return new ResponseEntity<>(users, OK); } @RequestMapping(method = PUT, value = DEV_FACILITIES) @TransactionalWrite public ResponseEntity<List<Facility>> pushFacilities(@RequestBody List<Facility> facilities) { FacilityDao facilityDao = (FacilityDao) facilityRepository; List<Facility> results = new ArrayList<>(); for (Facility facility : facilities) { if (facility.id != null) { facilityDao.insertFacility(facility, facility.id); } else { facility.id = facilityDao.insertFacility(facility); } results.add(facility); } devHelper.resetFacilitySequence(); return new ResponseEntity<>(results, OK); } @RequestMapping(method = PUT, value = DEV_UTILIZATION) @TransactionalWrite public ResponseEntity<Void> generateUtilizationData(@NotNull @PathVariable(FACILITY_ID) Long facilityId) { final Facility facility = facilityRepository.getFacility(facilityId); // Generate dummy usage for the last month final Random random = new Random(); final List<Utilization> utilizations = StreamSupport.stream( spliteratorUnknownSize(new DateTimeIterator(DateTime.now().minusMonths(1), DateTime.now(), Minutes.minutes(5)), Spliterator.ORDERED), false) .flatMap(ts -> facility.builtCapacity.keySet().stream() .flatMap(capacityType -> { if (facility.pricingMethod == PricingMethod.PARK_AND_RIDE_247_FREE) { return Stream.of(new UtilizationKey(facilityId, capacityType, Usage.PARK_AND_RIDE)); } else { return facility.pricing.stream() .filter(pr -> pr.capacityType == capacityType) .map(pr -> new UtilizationKey(facilityId, capacityType, pr.usage)); } }) .map(utilizationKey -> newUtilization( utilizationKey, facility.builtCapacity.get(utilizationKey.capacityType), ts.minusSeconds(random.nextInt(180)) // Randomness to prevent timestamps for different capacity types being equal ))) .collect(toList()); utilizationRepository.insertUtilizations(utilizations); predictionService.signalUpdateNeeded(utilizations); return new ResponseEntity<>(CREATED); } @RequestMapping(method = PUT, value = DEV_PREDICTION_HISTORY) @TransactionalRead // each call to predictionService.updatePredictionsHistoryForFacility creates a separate write transaction to avoid too long transactions public ResponseEntity<Void> generatePredictionHistory(@NotNull @PathVariable(FACILITY_ID) Long facilityId) { facilityRepository.getFacility(facilityId); // ensure facility exists final UtilizationSearch utilizationSearch = new UtilizationSearch(); utilizationSearch.start = DateTime.now().minusWeeks(5); utilizationSearch.end = DateTime.now(); utilizationSearch.facilityIds = Collections.singleton(facilityId); final List<Utilization> utilizations = Lists.newArrayList(utilizationRepository.findUtilizations(utilizationSearch)); DateTime lastTimestamp = utilizations.stream().map(u -> u.timestamp).max(DateTime::compareTo).orElse(utilizationSearch.end); StreamSupport.stream( spliteratorUnknownSize(new DateTimeIterator(utilizationSearch.start.plusWeeks(4), lastTimestamp.minus(PredictionRepository.PREDICTION_RESOLUTION), // avoid collision with scheduled predictions PredictionRepository.PREDICTION_RESOLUTION), Spliterator.ORDERED), false) .map(endTime -> utilizations.stream().filter(utilization -> utilization.timestamp.isBefore(endTime)).collect(toList())) .forEach(utilizationList -> predictionService.updatePredictionsHistoryForFacility(utilizationList)); return new ResponseEntity<>(CREATED); } @RequestMapping(method = PUT, value = DEV_PREDICTION) public ResponseEntity<Void> triggerPrediction() { predictionService.updatePredictions(); return new ResponseEntity<>(NO_CONTENT); } private static Utilization newUtilization(UtilizationKey utilizationKey, Integer maxCapacity, DateTime time) { final Utilization u = new Utilization(); u.capacityType = utilizationKey.capacityType; u.facilityId = utilizationKey.facilityId; u.timestamp = time; u.usage = utilizationKey.usage; u.spacesAvailable = sineWaveUtilization(time, maxCapacity); u.capacity = maxCapacity; return u; } private static Integer sineWaveUtilization(DateTime d, Integer maxCapacity) { // Peaks at 16, so we subtract 16 hours. final double x = (d.minusHours(16).getMinuteOfDay() * 2.0 * Math.PI) / (24.0 * 60.0); // 1 + cos(x) is in range 0..2 so we have to divide the max capacity by 2 final double usedSpaces = (maxCapacity / 2.0) * (1 + Math.cos(x)); return (int)(maxCapacity - usedSpaces); } @RequestMapping(method = PUT, value = DEV_HUBS) @TransactionalWrite public ResponseEntity<List<Hub>> pushHubs(@RequestBody List<Hub> hubs) { HubDao hubDao = (HubDao) hubRepository; List<Hub> results = new ArrayList<>(); for (Hub hub : hubs) { if (hub.id != null) { hubDao.insertHub(hub, hub.id); } else { hub.id = hubDao.insertHub(hub); } results.add(hub); } devHelper.resetHubSequence(); return new ResponseEntity<>(results, OK); } @RequestMapping(method = PUT, value = DEV_CONTACTS) @TransactionalWrite public ResponseEntity<List<Contact>> pushContacts(@RequestBody List<Contact> contacts) { ContactDao contactDao = (ContactDao) contactRepository; List<Contact> results = new ArrayList<>(); for (Contact contact : contacts) { if (contact.id != null) { contactDao.insertContact(contact, contact.id); } else { contact.id = contactDao.insertContact(contact); } results.add(contact); } devHelper.resetContactSequence(); return new ResponseEntity<>(results, OK); } @RequestMapping(method = PUT, value = DEV_OPERATORS) @TransactionalWrite public ResponseEntity<List<Operator>> pushOperators(@RequestBody List<Operator> operators) { OperatorDao operatorDao = (OperatorDao) operatorRepository; List<Operator> results = new ArrayList<>(); for (Operator operator : operators) { if (operator.id != null) { operatorDao.insertOperator(operator, operator.id); } else { operator.id = operatorDao.insertOperator(operator); } results.add(operator); } devHelper.resetOperatorSequence(); return new ResponseEntity<>(results, OK); } private static class DateTimeIterator implements Iterator<DateTime> { private DateTime current; private final DateTime end; private final ReadablePeriod interval; public DateTimeIterator(DateTime start, DateTime end, ReadablePeriod interval) { Assert.state(start.isBefore(end), "Start date must be before end date"); this.current = start; this.end = end; this.interval = interval; } @Override public boolean hasNext() { return current.isBefore(end); } @Override public DateTime next() { final DateTime returnable = this.current; this.current = this.current.plus(interval); return returnable; } } }