diff --git a/src/main/java/com/bannergress/backend/controllers/BannerController.java b/src/main/java/com/bannergress/backend/controllers/BannerController.java index e8c34cf7..b81b7b29 100644 --- a/src/main/java/com/bannergress/backend/controllers/BannerController.java +++ b/src/main/java/com/bannergress/backend/controllers/BannerController.java @@ -2,8 +2,10 @@ import com.bannergress.backend.dto.BannerDto; import com.bannergress.backend.dto.BannerSettingsDto; +import com.bannergress.backend.dto.MissionDto; import com.bannergress.backend.entities.Banner; import com.bannergress.backend.entities.BannerSettings; +import com.bannergress.backend.entities.Mission; import com.bannergress.backend.entities.PlaceInformation; import com.bannergress.backend.enums.BannerListType; import com.bannergress.backend.enums.BannerSortOrder; @@ -203,6 +205,8 @@ private BannerDto toSummary(Banner banner) { dto.id = banner.getCanonicalSlug(); dto.title = banner.getTitle(); dto.numberOfMissions = banner.getNumberOfMissions(); + dto.numberOfSubmittedMissions = banner.getNumberOfSubmittedMissions(); + dto.numberOfDisabledMissions = banner.getNumberOfDisabledMissions(); dto.lengthMeters = banner.getLengthMeters(); dto.startLatitude = getLatitude(banner.getStartPoint()); dto.startLongitude = getLongitude(banner.getStartPoint()); @@ -219,12 +223,16 @@ private BannerDto toSummary(Banner banner) { private BannerDto toDetails(Banner banner) { BannerDto dto = toSummary(banner); - dto.missions = Maps.transformValues(banner.getMissions(), MissionController::toDetails); + dto.missions = Maps.transformValues(banner.getMissionsAndPlaceholders(), this::toMissionOrPlaceholder); dto.type = banner.getType(); dto.description = banner.getDescription(); return dto; } + private MissionDto toMissionOrPlaceholder(Optional input) { + return input.map(MissionController::toDetails).orElse(new MissionDto()); + } + private void amendUserSettings(Principal principal, Collection bannerDtos) { if (principal != null) { List bannerSettings = bannerSettingsService.getBannerSettings(principal.getName(), diff --git a/src/main/java/com/bannergress/backend/dto/BannerDto.java b/src/main/java/com/bannergress/backend/dto/BannerDto.java index 61e61f6e..c024ff25 100644 --- a/src/main/java/com/bannergress/backend/dto/BannerDto.java +++ b/src/main/java/com/bannergress/backend/dto/BannerDto.java @@ -47,6 +47,16 @@ public class BannerDto { */ public int numberOfMissions; + /** + * Number of submitted missions. + */ + public int numberOfSubmittedMissions; + + /** + * Number of disabled missions. + */ + public int numberOfDisabledMissions; + /** * Map between the zero-based mission position and the mission. The mission * position must be less than {@link #numberOfMissions}. The map may be sparse, diff --git a/src/main/java/com/bannergress/backend/entities/Banner.java b/src/main/java/com/bannergress/backend/entities/Banner.java index 342f4f76..2abfcc60 100644 --- a/src/main/java/com/bannergress/backend/entities/Banner.java +++ b/src/main/java/com/bannergress/backend/entities/Banner.java @@ -2,6 +2,8 @@ import com.bannergress.backend.enums.BannerType; import com.bannergress.backend.utils.PojoBuilder; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Maps; import net.karneim.pojobuilder.GeneratePojoBuilder; import org.hibernate.annotations.NaturalId; import org.hibernate.annotations.SortNatural; @@ -73,6 +75,20 @@ public class Banner { @NotAudited private int numberOfMissions; + /** + * Number of submitted ("not yet published") missions. + */ + @Column(name = "number_of_submitted_missions", nullable = false) + @NotAudited + private int numberOfSubmittedMissions; + + /** + * Number of disabled ("not published anymore") missions. + */ + @Column(name = "number_of_disabled_missions", nullable = false) + @NotAudited + private int numberOfDisabledMissions; + /** * Map between the zero-based mission position and the mission. The mission * position must be less than {@link #numberOfMissions}. The map may be sparse, @@ -86,6 +102,16 @@ public class Banner { @SortNatural private SortedMap missions = new TreeMap<>(); + /** + * Set of zero-based mission positions where a mission is supposed to be. This set and the keyset of {@link #missions} are mutually exclusive. + */ + @ElementCollection + @CollectionTable(name = "banner_placeholder", joinColumns = {@JoinColumn(name = "banner")}) + @Column(name = "position") + @AuditJoinTable(name = "banner_placeholder_audit") + @SortNatural + private SortedSet placeholders = new TreeSet<>(); + /** * Start portal of the first mission. */ @@ -101,13 +127,6 @@ public class Banner { @NotAudited private Integer lengthMeters; - /** - * All mission information is present. - */ - @Column(name = "complete", nullable = false) - @NotAudited - private boolean complete; - /** * All missions are online. */ @@ -210,6 +229,22 @@ public void setNumberOfMissions(int numberOfMissions) { this.numberOfMissions = numberOfMissions; } + public int getNumberOfSubmittedMissions() { + return numberOfSubmittedMissions; + } + + public void setNumberOfSubmittedMissions(int numberOfSubmittedMissions) { + this.numberOfSubmittedMissions = numberOfSubmittedMissions; + } + + public int getNumberOfDisabledMissions() { + return numberOfDisabledMissions; + } + + public void setNumberOfDisabledMissions(int numberOfDisabledMissions) { + this.numberOfDisabledMissions = numberOfDisabledMissions; + } + public SortedMap getMissions() { return missions; } @@ -218,6 +253,20 @@ public void setMissions(SortedMap missions) { this.missions = missions; } + public SortedSet getPlaceholders() { + return placeholders; + } + + public void setPlaceholders(SortedSet placeholders) { + this.placeholders = placeholders; + } + + public ImmutableSortedMap> getMissionsAndPlaceholders() { + return ImmutableSortedMap.>naturalOrder() + .putAll(Maps.transformValues(missions, Optional::of)) + .putAll(Maps.asMap(placeholders, p -> Optional.empty())).build(); + } + public Point getStartPoint() { return startPoint; } @@ -242,14 +291,6 @@ public void setStartPlaces(Set startPlaces) { this.startPlaces = startPlaces; } - public boolean isComplete() { - return complete; - } - - public void setComplete(boolean complete) { - this.complete = complete; - } - public boolean isOnline() { return online; } @@ -292,7 +333,7 @@ public boolean equals(final Object o) { } final Banner banner = (Banner) o; return Objects.equals(uuid, banner.uuid) && numberOfMissions == banner.numberOfMissions - && complete == banner.complete && online == banner.online && Objects.equals(title, banner.title) + && online == banner.online && Objects.equals(title, banner.title) && Objects.equals(description, banner.description) && Objects.equals(missions, banner.missions) && Objects.equals(startPoint, banner.startPoint) && Objects.equals(lengthMeters, banner.lengthMeters) && Objects.equals(picture, banner.picture) && Objects.equals(startPlaces, banner.startPlaces) @@ -302,7 +343,7 @@ public boolean equals(final Object o) { @Override public int hashCode() { - return Objects.hash(uuid, title, description, numberOfMissions, missions, startPoint, lengthMeters, complete, - online, picture, startPlaces, created, type, canonicalSlug); + return Objects.hash(uuid, title, description, numberOfMissions, missions, startPoint, lengthMeters, online, + picture, startPlaces, created, type, canonicalSlug); } } diff --git a/src/main/java/com/bannergress/backend/services/impl/BannerPictureServiceImpl.java b/src/main/java/com/bannergress/backend/services/impl/BannerPictureServiceImpl.java index 11a5e1d8..fdac71ab 100644 --- a/src/main/java/com/bannergress/backend/services/impl/BannerPictureServiceImpl.java +++ b/src/main/java/com/bannergress/backend/services/impl/BannerPictureServiceImpl.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Map.Entry; import java.util.Optional; +import java.util.SortedMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; @@ -106,6 +107,9 @@ private String hash(Banner banner) { hasher.putInt(entry.getKey()).putUnencodedChars(entry.getValue().getPicture().toString()) .putBoolean(entry.getValue().getStatus() == MissionStatus.published); } + for (Integer position : banner.getPlaceholders()) { + hasher.putInt(position); + } return hasher.hash().toString(); } @@ -122,7 +126,8 @@ private static BufferedImage loadImage(String path) { protected byte[] createPicture(Banner banner) { final int numberColumns = banner.getWidth(); - final int numberRows = banner.getMissions().lastKey() / numberColumns + 1; + SortedMap> missionsAndPlaceholders = banner.getMissionsAndPlaceholders(); + final int numberRows = missionsAndPlaceholders.lastKey() / numberColumns + 1; final int DISTANCE_CIRCLES = 4; final int DIAMETER = 96; final int MISSIONSIZE = DIAMETER + DISTANCE_CIRCLES; @@ -136,24 +141,27 @@ protected byte[] createPicture(Banner banner) { // loads, draws and masks the individual mission images to the banner image. try { - threadPool.submit(() -> banner.getMissions().entrySet().parallelStream().forEach(entry -> { - BufferedImage missionImage; - Request request = new Request.Builder().url(entry.getValue().getPicture()).build(); - try (Response response = client.newCall(request).execute()) { - missionImage = ImageIO.read(response.body().byteStream()); - } catch (IOException ex) { - throw new RuntimeException("failed ro read image: " + entry.getValue().getPicture(), ex); - } + threadPool.submit(() -> missionsAndPlaceholders.entrySet().parallelStream().forEach(entry -> { + Optional optionalMissionImage = entry.getValue().map(mission -> { + Request request = new Request.Builder().url(mission.getPicture()).build(); + try (Response response = client.newCall(request).execute()) { + return ImageIO.read(response.body().byteStream()); + } catch (IOException ex) { + throw new RuntimeException("failed ro read image: " + mission.getPicture(), ex); + } + }); int missionPosition = numberColumns * numberRows - entry.getKey().intValue() - 1; int x1 = DISTANCE_CIRCLES + (missionPosition % numberColumns) * MISSIONSIZE; int y1 = DISTANCE_CIRCLES + (missionPosition / numberColumns) * MISSIONSIZE; int x2 = x1 + DIAMETER; int y2 = y1 + DIAMETER; synchronized (graphics) { - graphics.drawImage(missionImage, x1, y1, x2, y2, 0, 0, missionImage.getWidth(), - missionImage.getHeight(), null); - BufferedImage maskImage = entry.getValue().getStatus() == MissionStatus.published ? maskImageOnline - : maskImageOffline; + optionalMissionImage.ifPresent(missionImage -> { + graphics.drawImage(missionImage, x1, y1, x2, y2, 0, 0, missionImage.getWidth(), + missionImage.getHeight(), null); + }); + MissionStatus status = entry.getValue().map(Mission::getStatus).orElse(MissionStatus.submitted); + BufferedImage maskImage = status == MissionStatus.published ? maskImageOnline : maskImageOffline; graphics.drawImage(maskImage, x1, y1, x2, y2, 0, 0, maskImage.getWidth(), maskImage.getHeight(), null); } diff --git a/src/main/java/com/bannergress/backend/services/impl/BannerServiceImpl.java b/src/main/java/com/bannergress/backend/services/impl/BannerServiceImpl.java index 7975f587..2ab702ba 100644 --- a/src/main/java/com/bannergress/backend/services/impl/BannerServiceImpl.java +++ b/src/main/java/com/bannergress/backend/services/impl/BannerServiceImpl.java @@ -1,13 +1,13 @@ package com.bannergress.backend.services.impl; import com.bannergress.backend.dto.BannerDto; +import com.bannergress.backend.dto.MissionDto; import com.bannergress.backend.entities.Banner; import com.bannergress.backend.entities.Mission; import com.bannergress.backend.entities.MissionStep; import com.bannergress.backend.entities.Place; import com.bannergress.backend.enums.BannerListType; import com.bannergress.backend.enums.BannerSortOrder; -import com.bannergress.backend.enums.MissionStatus; import com.bannergress.backend.exceptions.MissionAlreadyUsedException; import com.bannergress.backend.repositories.BannerRepository; import com.bannergress.backend.repositories.BannerSpecifications; @@ -198,8 +198,10 @@ private String deriveSlug(Banner banner) { private Banner createTransient(BannerDto bannerDto, List acceptableBannerSlugs) throws MissionAlreadyUsedException { - Collection missionIds = Collections2.transform(bannerDto.missions.values(), - missionDto -> missionDto.id); + Map missions = Maps.filterValues(bannerDto.missions, missionDto -> missionDto.id != null); + Map placeholders = Maps.filterValues(bannerDto.missions, + missionDto -> missionDto.id == null); + Collection missionIds = Collections2.transform(missions.values(), missionDto -> missionDto.id); missionService.assertNotAlreadyUsedInBanners(missionIds, acceptableBannerSlugs); Banner banner = new Banner(); banner.setTitle(bannerDto.title); @@ -209,7 +211,8 @@ private Banner createTransient(BannerDto bannerDto, List acceptableBanne banner.setType(bannerDto.type); banner.getMissions().clear(); banner.getMissions() - .putAll(Maps.transformValues(bannerDto.missions, missionDto -> missionRepository.getOne(missionDto.id))); + .putAll(Maps.transformValues(missions, missionDto -> missionRepository.getOne(missionDto.id))); + banner.getPlaceholders().addAll(placeholders.keySet()); calculateData(banner); pictureService.refresh(banner); return banner; @@ -252,13 +255,19 @@ public void deleteBySlug(String slug) { @Override public void calculateData(Banner banner) { Point startPoint = null; - boolean complete = true; - boolean online = complete; + int numberOfSubmittedMissions = banner.getPlaceholders().size(); + int numberOfDisabledMissions = 0; for (Mission mission : banner.getMissions().values()) { - online &= mission.getStatus() == MissionStatus.published; - if (mission.getType() == null) { - complete = false; + switch (mission.getStatus()) { + case disabled: + numberOfDisabledMissions++; + break; + case submitted: + numberOfSubmittedMissions++; + break; + default: + break; } for (MissionStep step : mission.getSteps()) { if (step.getPoi() != null) { @@ -270,9 +279,10 @@ public void calculateData(Banner banner) { } } banner.setStartPoint(startPoint); - banner.setComplete(complete); - banner.setOnline(online); - banner.setNumberOfMissions(banner.getMissions().size()); + banner.setOnline(numberOfSubmittedMissions == 0 && numberOfDisabledMissions == 0); + banner.setNumberOfMissions(banner.getMissions().size() + banner.getPlaceholders().size()); + banner.setNumberOfDisabledMissions(numberOfDisabledMissions); + banner.setNumberOfSubmittedMissions(numberOfSubmittedMissions); if (startPoint != null && banner.getStartPlaces().isEmpty()) { Collection startPlaces = placesService.getPlaces(startPoint); banner.getStartPlaces().clear(); diff --git a/src/main/resources/db/migration/V2_13__banner_status.sql b/src/main/resources/db/migration/V2_13__banner_status.sql new file mode 100644 index 00000000..2c81bbb0 --- /dev/null +++ b/src/main/resources/db/migration/V2_13__banner_status.sql @@ -0,0 +1,28 @@ +ALTER TABLE banner DROP COLUMN complete; +ALTER TABLE banner ADD COLUMN number_of_submitted_missions integer; +ALTER TABLE banner ADD COLUMN number_of_disabled_missions integer; +UPDATE banner b SET + number_of_submitted_missions = 0, + number_of_disabled_missions = ( + SELECT COUNT(*) FROM banner_mission bm + JOIN mission m ON bm.mission = m.id + WHERE bm.banner = b.uuid AND m.status = 'disabled' + ); +ALTER TABLE BANNER ALTER COLUMN number_of_submitted_missions SET NOT NULL; +ALTER TABLE BANNER ALTER COLUMN number_of_disabled_missions SET NOT NULL; + +CREATE TABLE banner_placeholder ( + banner uuid NOT NULL, + position integer NOT NULL, + PRIMARY KEY (banner, position), + FOREIGN KEY (banner) REFERENCES banner (uuid) +); + +CREATE TABLE banner_placeholder_audit ( + rev integer NOT NULL, + banner uuid NOT NULL, + position integer NOT NULL, + revtype smallint, + PRIMARY KEY (rev, banner, position), + FOREIGN KEY (rev) REFERENCES revision (id) +); diff --git a/src/test/java/com/bannergress/backend/services/impl/TestBannerPictureServiceImpl.java b/src/test/java/com/bannergress/backend/services/impl/TestBannerPictureServiceImpl.java index 89278d0e..7d80a13b 100644 --- a/src/test/java/com/bannergress/backend/services/impl/TestBannerPictureServiceImpl.java +++ b/src/test/java/com/bannergress/backend/services/impl/TestBannerPictureServiceImpl.java @@ -3,6 +3,7 @@ import com.bannergress.backend.entities.Banner; import com.bannergress.backend.entities.Mission; import com.bannergress.backend.enums.MissionStatus; +import org.assertj.core.util.Sets; import org.junit.jupiter.api.Test; import java.io.File; @@ -37,6 +38,7 @@ void testCreatePicture() throws IOException { } banner.setMissions(missions); banner.getMissions().get(3).setStatus(MissionStatus.disabled); + banner.setPlaceholders(Sets.newTreeSet(6, 11, 12, 17, 18, 19, 20, 21, 22, 23)); byte[] pngData = bannerPictureService.createPicture(banner); diff --git a/src/test/java/com/bannergress/backend/testutils/builder/EntityBuilder.java b/src/test/java/com/bannergress/backend/testutils/builder/EntityBuilder.java index 961a9e08..8749fdcb 100644 --- a/src/test/java/com/bannergress/backend/testutils/builder/EntityBuilder.java +++ b/src/test/java/com/bannergress/backend/testutils/builder/EntityBuilder.java @@ -22,7 +22,6 @@ public class EntityBuilder { .withNumberOfMissions(a($Int())) .withMissions(sortedMapWith(0, $Mission())) .withLengthMeters(a($Int())) - .withComplete(a($Boolean())) .withOnline(a($Boolean())) .withPicture(a($BannerPicture())) .withStartPlaces(setWith($Place()))