diff --git a/covid19-model/src/main/java/com/bvn13/covid19/model/entities/CovidStat.java b/covid19-model/src/main/java/com/bvn13/covid19/model/entities/CovidStat.java index 2b70d1c..0fceb05 100644 --- a/covid19-model/src/main/java/com/bvn13/covid19/model/entities/CovidStat.java +++ b/covid19-model/src/main/java/com/bvn13/covid19/model/entities/CovidStat.java @@ -24,7 +24,7 @@ import java.time.LocalDateTime; @Data @Entity @Table(schema = "covid", name = "cvd_stats") -public class CovidStat { +public class CovidStat implements StatsProvider { @Id @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "cvd_stats_seq") diff --git a/covid19-model/src/main/java/com/bvn13/covid19/model/entities/StatsProvider.java b/covid19-model/src/main/java/com/bvn13/covid19/model/entities/StatsProvider.java new file mode 100644 index 0000000..4e6b4f0 --- /dev/null +++ b/covid19-model/src/main/java/com/bvn13/covid19/model/entities/StatsProvider.java @@ -0,0 +1,23 @@ +/* +Copyright [2020] [bvn13] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + +package com.bvn13.covid19.model.entities; + +public interface StatsProvider { + long getSick(); + long getHealed(); + long getDied(); +} diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/AllStatsController.java b/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/AllStatsController.java index ffd60d2..f92b6e8 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/AllStatsController.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/AllStatsController.java @@ -1,11 +1,30 @@ +/* +Copyright [2020] [bvn13] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + package com.bvn13.covid19.site.controllers; import com.bvn13.covid19.model.entities.CovidStat; +import com.bvn13.covid19.model.entities.CovidUpdate; +import com.bvn13.covid19.model.entities.Region; +import com.bvn13.covid19.model.entities.StatsProvider; import com.bvn13.covid19.site.model.CovidAllStats; -import com.bvn13.covid19.site.model.CovidData; import com.bvn13.covid19.site.model.CovidDayStats; -import com.bvn13.covid19.site.service.CovidStatsMaker; -import com.bvn13.covid19.site.service.CovidStatsResponseMaker; +import com.bvn13.covid19.site.repositories.RegionsRepository; +import com.bvn13.covid19.site.service.StatisticsPreparator; +import com.bvn13.covid19.site.service.StatisticsAggregator; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; @@ -16,10 +35,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.PostConstruct; import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; @RequiredArgsConstructor @@ -27,8 +43,9 @@ import java.util.stream.Collectors; @RequestMapping("/stats") public class AllStatsController { - private final CovidStatsMaker covidStatsMaker; - private final CovidStatsResponseMaker covidStatsResponseMaker; + private final StatisticsPreparator statisticsPreparator; + private final StatisticsAggregator statisticsAggregator; + private final RegionsRepository regionsRepository; @Value("${app.zone-id}") private String zoneIdStr; @@ -51,45 +68,53 @@ public class AllStatsController { } private CovidAllStats constructResponseForRegion(String regionName) { - return CovidAllStats.builder() - .regions(Collections.singletonList(regionName)) - .progress(covidStatsMaker.findAllLastUpdatesPerDay().stream() - .map(covidUpdate -> CovidDayStats.builder() - .datetime(covidUpdate.getDatetime()) - .updatedOn(covidUpdate.getCreatedOn().atZone(zoneId)) - .stats(convertStatsToData(findCovidStatsByUpdateIdAndRegion(covidUpdate.getId(), regionName))) - .build()) - .sorted(CovidDayStats::compareTo) - .collect(Collectors.toList())) - .build(); + return regionsRepository.findFirstByName(regionName) + .map(region -> CovidAllStats.builder() + .regions(Collections.singletonList(region.getName())) + .progress(compoundProgressForRegions(Collections.singletonList(region))) + .build() + ) + .orElse(CovidAllStats.builder() + .regions(Collections.singletonList(regionName)) + .progress(Collections.emptyList()) + .build() + ); } private CovidAllStats constructResponseForAllRegions() { return CovidAllStats.builder() - .regions(covidStatsMaker.findAllRegionsNames()) - .progress(covidStatsMaker.findAllLastUpdatesPerDay().stream() - .map(covidUpdate -> CovidDayStats.builder() - .datetime(covidUpdate.getDatetime()) - .updatedOn(covidUpdate.getCreatedOn().atZone(zoneId)) - .stats(convertStatsToData(findCovidStatsByUpdateId(covidUpdate.getId()))) - .build()) - .sorted(CovidDayStats::compareTo) - .collect(Collectors.toList())) + .regions(statisticsPreparator.findAllRegionsNames()) + .progress(compoundProgressForRegions(regionsRepository.findAll())) .build(); } - private List convertStatsToData(Collection stats) { - return covidStatsResponseMaker.convertStats(stats); - } - - private List findCovidStatsByUpdateIdAndRegion(long updateId, String region) { - return covidStatsMaker.findCovidStatsByUpdateInfoId(updateId).stream() - .filter(covidStat -> region.equals(covidStat.getRegion().getName())) + private List compoundProgressForRegions(List regions) { + return statisticsPreparator.findAllLastUpdatesPerDay().stream() + .map(covidUpdate -> prepareDayStats(covidUpdate, regions)) + .sorted(CovidDayStats::compareTo) .collect(Collectors.toList()); } - private List findCovidStatsByUpdateId(long updateId) { - return new ArrayList<>(covidStatsMaker.findCovidStatsByUpdateInfoId(updateId)); + private CovidDayStats prepareDayStats(CovidUpdate currentUpdate, List regions) { + List currentStats = findCovidStatsByUpdateIdAndRegion(currentUpdate.getId(), regions); + Optional prevUpdate = statisticsPreparator.findPrevUpdateByDate(currentUpdate.getDatetime()); + Map prevStats = prevUpdate + .map(covidUpdate -> statisticsPreparator.findCovidStatsByUpdateInfoId(covidUpdate.getId()).stream() + .collect(Collectors.toMap(CovidStat::getRegion, (cs) -> (StatsProvider) cs)) + ) + .orElseGet(() -> new HashMap<>(0)); + + return CovidDayStats.builder() + .datetime(currentUpdate.getDatetime()) + .updatedOn(currentUpdate.getCreatedOn().atZone(zoneId)) + .stats(statisticsAggregator.prepareStats(currentStats, prevStats)) + .build(); + } + + private List findCovidStatsByUpdateIdAndRegion(long updateId, List regions) { + return statisticsPreparator.findCovidStatsByUpdateInfoId(updateId).stream() + .filter(covidStat -> regions.contains(covidStat.getRegion())) + .collect(Collectors.toList()); } } diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/LastStatsController.java b/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/LastStatsController.java index a495d33..0e26bf5 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/LastStatsController.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/LastStatsController.java @@ -16,9 +16,13 @@ limitations under the License. package com.bvn13.covid19.site.controllers; +import com.bvn13.covid19.model.entities.CovidStat; +import com.bvn13.covid19.model.entities.CovidUpdate; +import com.bvn13.covid19.model.entities.Region; +import com.bvn13.covid19.model.entities.StatsProvider; import com.bvn13.covid19.site.model.CovidDayStats; -import com.bvn13.covid19.site.service.CovidStatsMaker; -import com.bvn13.covid19.site.service.CovidStatsResponseMaker; +import com.bvn13.covid19.site.service.StatisticsPreparator; +import com.bvn13.covid19.site.service.StatisticsAggregator; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; @@ -27,14 +31,19 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.PostConstruct; import java.time.ZoneId; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; @RequiredArgsConstructor @RestController @RequestMapping("/stats") public class LastStatsController { - private final CovidStatsMaker covidStatsMaker; - private final CovidStatsResponseMaker covidStatsResponseMaker; + private final StatisticsPreparator statisticsPreparator; + private final StatisticsAggregator statisticsAggregator; @Value("${app.zone-id}") private String zoneIdStr; @@ -47,13 +56,24 @@ public class LastStatsController { @GetMapping("/last") public CovidDayStats getStatistics() { - return covidStatsMaker.findLastUpdate() - .map(covidUpdate -> CovidDayStats.builder() - .datetime(covidUpdate.getDatetime()) - .updatedOn(covidUpdate.getCreatedOn().atZone(zoneId)) - .stats(covidStatsResponseMaker.convertStats(covidStatsMaker.findCovidStatsByUpdateInfoId(covidUpdate.getId()))) - .build()) - .orElse(CovidDayStats.builder().build()); + Optional lastUpdate = statisticsPreparator.findLastUpdate(); + if (lastUpdate.isPresent()) { + Collection currentStats = statisticsPreparator.findCovidStatsByUpdateInfoId(lastUpdate.get().getId()); + Optional prevUpdate = statisticsPreparator.findPrevUpdateByDate(lastUpdate.get().getDatetime()); + Map prevStats = prevUpdate + .map(covidUpdate -> statisticsPreparator.findCovidStatsByUpdateInfoId(covidUpdate.getId()).stream() + .collect(Collectors.toMap(CovidStat::getRegion, (cs) -> (StatsProvider) cs)) + ) + .orElseGet(() -> new HashMap<>(0)); + + return CovidDayStats.builder() + .datetime(lastUpdate.get().getDatetime()) + .updatedOn(lastUpdate.get().getCreatedOn().atZone(zoneId)) + .stats(statisticsAggregator.prepareStats(currentStats, prevStats)) + .build(); + } else { + return CovidDayStats.builder().build(); + } } } diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/RegionsController.java b/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/RegionsController.java index 468fc04..66434b0 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/RegionsController.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/controllers/RegionsController.java @@ -1,6 +1,22 @@ +/* +Copyright [2020] [bvn13] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + package com.bvn13.covid19.site.controllers; -import com.bvn13.covid19.site.service.CovidStatsMaker; +import com.bvn13.covid19.site.service.StatisticsPreparator; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,11 +29,11 @@ import java.util.List; @RequestMapping("/regions") public class RegionsController { - private final CovidStatsMaker covidStatsMaker; + private final StatisticsPreparator statisticsPreparator; @GetMapping public List getAllRegions() { - return covidStatsMaker.findAllRegionsNames(); + return statisticsPreparator.findAllRegionsNames(); } } diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/model/CovidData.java b/covid19-site/src/main/java/com/bvn13/covid19/site/model/CovidData.java index d1ff6eb..761058e 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/model/CovidData.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/model/CovidData.java @@ -16,16 +16,27 @@ limitations under the License. package com.bvn13.covid19.site.model; +import com.bvn13.covid19.model.entities.StatsProvider; import lombok.Builder; import lombok.Value; -@Builder +@Builder(toBuilder = true) @Value -public class CovidData { +public class CovidData implements StatsProvider { String region; long sick; long healed; long died; + Delta previous; + + @Builder + @Value + public static class Delta implements StatsProvider { + long sick; + long healed; + long died; + } + } diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/CovidUpdatesRepository.java b/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/CovidUpdatesRepository.java index 5d2aae0..794b44e 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/CovidUpdatesRepository.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/CovidUpdatesRepository.java @@ -20,9 +20,12 @@ import com.bvn13.covid19.site.dtos.CovidUpdateInfoDto; import com.bvn13.covid19.model.entities.CovidUpdate; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.Collection; import java.util.Optional; @@ -32,6 +35,9 @@ public interface CovidUpdatesRepository extends JpaRepository @Query("select new com.bvn13.covid19.site.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U") Optional findLastUpdate(); + @Query("select U from CovidUpdate U where U.datetime >= :date1 and U.datetime < :date2") + Optional findByDateOfUpdate(@Param("date1") ZonedDateTime date1, @Param("date2") ZonedDateTime date2); + @Query("select U from CovidUpdate U where U.id in (select max(U1.id) from CovidUpdate U1 group by U1.datetime)") Collection findAllLastUpdatesPerDay(); diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/RegionsRepository.java b/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/RegionsRepository.java index fdb3b85..a609fa4 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/RegionsRepository.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/repositories/RegionsRepository.java @@ -4,6 +4,9 @@ import com.bvn13.covid19.model.entities.Region; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface RegionsRepository extends JpaRepository { + Optional findFirstByName(String name); } diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/service/CovidStatsResponseMaker.java b/covid19-site/src/main/java/com/bvn13/covid19/site/service/CovidStatsResponseMaker.java deleted file mode 100644 index 2746bc4..0000000 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/service/CovidStatsResponseMaker.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.bvn13.covid19.site.service; - -import com.bvn13.covid19.model.entities.CovidStat; -import com.bvn13.covid19.site.model.CovidData; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -@Component -public class CovidStatsResponseMaker { - - public List convertStats(Collection stats) { - return stats.stream() - .map(stat -> CovidData.builder() - .region(stat.getRegion().getName()) - .sick(stat.getSick()) - .healed(stat.getHealed()) - .died(stat.getDied()) - .build()) - .collect(Collectors.toList()); - } - -} diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/service/StatisticsAggregator.java b/covid19-site/src/main/java/com/bvn13/covid19/site/service/StatisticsAggregator.java new file mode 100644 index 0000000..bb2abe9 --- /dev/null +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/service/StatisticsAggregator.java @@ -0,0 +1,63 @@ +/* +Copyright [2020] [bvn13] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + +package com.bvn13.covid19.site.service; + +import com.bvn13.covid19.model.entities.CovidStat; +import com.bvn13.covid19.model.entities.Region; +import com.bvn13.covid19.model.entities.StatsProvider; +import com.bvn13.covid19.site.model.CovidData; +import lombok.RequiredArgsConstructor; +import org.springframework.data.util.Pair; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class StatisticsAggregator { + + private static final StatsProvider zero = CovidData.Delta.builder() + .sick(0L) + .healed(0L) + .died(0L) + .build(); + + + public List prepareStats(Collection current, Map previous) { + return current.stream() + .map(stat -> Pair.of(stat, Optional.ofNullable(previous.get(stat.getRegion())))) + .map(statPair -> CovidData.builder() + .region(statPair.getFirst().getRegion().getName()) + .sick(statPair.getFirst().getSick()) + .healed(statPair.getFirst().getHealed()) + .died(statPair.getFirst().getDied()) + .previous(CovidData.Delta.builder() + .sick(statPair.getSecond().orElse(zero).getSick()) + .healed(statPair.getSecond().orElse(zero).getHealed()) + .died(statPair.getSecond().orElse(zero).getDied()) + .build() + ) + .build() + ) + .collect(Collectors.toList()); + } + +} diff --git a/covid19-site/src/main/java/com/bvn13/covid19/site/service/CovidStatsMaker.java b/covid19-site/src/main/java/com/bvn13/covid19/site/service/StatisticsPreparator.java similarity index 62% rename from covid19-site/src/main/java/com/bvn13/covid19/site/service/CovidStatsMaker.java rename to covid19-site/src/main/java/com/bvn13/covid19/site/service/StatisticsPreparator.java index 467e894..072e9f9 100644 --- a/covid19-site/src/main/java/com/bvn13/covid19/site/service/CovidStatsMaker.java +++ b/covid19-site/src/main/java/com/bvn13/covid19/site/service/StatisticsPreparator.java @@ -23,10 +23,17 @@ import com.bvn13.covid19.site.repositories.CovidStatsRepository; import com.bvn13.covid19.site.repositories.CovidUpdatesRepository; import com.bvn13.covid19.site.repositories.RegionsRepository; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; import javax.transaction.Transactional; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -35,12 +42,30 @@ import java.util.stream.Collectors; @RequiredArgsConstructor @Component -public class CovidStatsMaker { +public class StatisticsPreparator { private final CovidStatsRepository statsRepository; private final CovidUpdatesRepository updatesRepository; private final RegionsRepository regionsRepository; + private LocalDate projectStartDate; + private ZonedDateTime projectStartZonedDate; + @Value("${app.zone-id}") + private String zoneIdStr; + + @PostConstruct + public void init() { + ZoneId zoneId = ZoneId.of(zoneIdStr); + projectStartZonedDate = projectStartDate.atTime(0, 0, 0).atZone(zoneId); + } + + @Value("${app.project-start-date}") + private void setProjectStartDate(String ld) { + if (ld != null && !ld.isEmpty()) { + projectStartDate = LocalDate.parse(ld); + } + } + @Transactional public Collection getLastCovidStats() { return updatesRepository.findLastUpdate() @@ -59,6 +84,26 @@ public class CovidStatsMaker { .flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn())); } + @Cacheable( + cacheNames = "covid-prev-update-by-date", + unless = "#result == null" + ) + public Optional findPrevUpdateByDate(ZonedDateTime date) { + if (date.isBefore(projectStartZonedDate)) { + return Optional.empty(); + } else { + Optional update = updatesRepository.findByDateOfUpdate( + dateWithTime(prevDate(date), 0, 0, 0), + dateWithTime(date, 0, 0, 0) + ); + if (update.isPresent()) { + return update; + } else { + return findPrevUpdateByDate(prevDate(date)); + } + } + } + @Cacheable( cacheNames = "covid-stats-by-update-info-id", condition = "#updateInfoId > 0", @@ -84,4 +129,15 @@ public class CovidStatsMaker { return regionsRepository.findAll().stream().map(Region::getName).collect(Collectors.toList()); } + private ZonedDateTime prevDate(ZonedDateTime date) { + return date.minus(1, ChronoUnit.DAYS); + } + + private ZonedDateTime dateWithTime(ZonedDateTime date, int hour, int minute, int second) { + return date + .withHour(hour) + .withMinute(minute) + .withSecond(second); + } + } diff --git a/covid19-site/src/main/resources/application.yaml b/covid19-site/src/main/resources/application.yaml index 395d209..45f8c36 100644 --- a/covid19-site/src/main/resources/application.yaml +++ b/covid19-site/src/main/resources/application.yaml @@ -4,6 +4,7 @@ server: app: zone-id: Europe/Moscow main-url: http://localhost:8080 + project-start-date: 2020-03-01 spring: application: @@ -13,7 +14,7 @@ spring: type: caffeine caffeine: spec: expireAfterWrite=15m - cache-names: covid-last-update, covid-all-days-updates, covid-stats-by-update-info-id, covid-regions + cache-names: covid-last-update, covid-all-days-updates, covid-stats-by-update-info-id, covid-regions, covid-prev-update-by-date flyway: enabled: false diff --git a/covid19-site/src/main/resources/templates/main-page.html b/covid19-site/src/main/resources/templates/main-page.html index 02fb6b9..d7be108 100644 --- a/covid19-site/src/main/resources/templates/main-page.html +++ b/covid19-site/src/main/resources/templates/main-page.html @@ -2,7 +2,7 @@ - Home page + Статистика COVID19 в России @@ -28,6 +28,13 @@ +

+ Исходный код +

+

+ @bvn13 - Автор проекта +

+