From e9c9fb373e2b0e08cec0ad24fb835da98068f328 Mon Sep 17 00:00:00 2001 From: tomassirio Date: Tue, 31 Mar 2026 16:50:47 +0200 Subject: [PATCH] feat: Add cache-busting support for trip thumbnail URLs based on polyline update timestamp --- .../wanderer/commons/dto/TripDTO.java | 3 +- .../wanderer/commons/dto/TripSummaryDTO.java | 6 +- .../commons/service/ThumbnailUrlService.java | 34 +++++++++-- .../service/ThumbnailUrlServiceTest.java | 61 ++++++++++++++++--- .../service/helper/TripEnrichmentHelper.java | 3 +- .../query/controller/TripControllerTest.java | 3 +- .../service/impl/SearchServiceImplTest.java | 6 +- 7 files changed, 98 insertions(+), 18 deletions(-) diff --git a/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripDTO.java b/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripDTO.java index 27fec3c4..f97aeabb 100644 --- a/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripDTO.java +++ b/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripDTO.java @@ -32,6 +32,7 @@ public record TripDTO( @JsonProperty("thumbnailUrl") public String thumbnailUrl() { boolean hasUpdates = updateCount != null && updateCount > 0; - return ThumbnailUrlService.resolveTripThumbnailUrl(id, hasUpdates, tripPlanId); + Long timestamp = polylineUpdatedAt != null ? polylineUpdatedAt.getEpochSecond() : null; + return ThumbnailUrlService.resolveTripThumbnailUrl(id, hasUpdates, tripPlanId, timestamp); } } diff --git a/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripSummaryDTO.java b/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripSummaryDTO.java index 700d974b..c9568e03 100644 --- a/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripSummaryDTO.java +++ b/commons/src/main/java/com/tomassirio/wanderer/commons/dto/TripSummaryDTO.java @@ -24,11 +24,13 @@ public record TripSummaryDTO( Boolean isPromoted, Instant promotedAt, Boolean isPreAnnounced, - Instant countdownStartDate) { + Instant countdownStartDate, + Instant polylineUpdatedAt) { // For cache-busting @JsonProperty("thumbnailUrl") public String thumbnailUrl() { boolean hasUpdates = updateCount != null && updateCount > 0; - return ThumbnailUrlService.resolveTripThumbnailUrl(id, hasUpdates, tripPlanId); + Long timestamp = polylineUpdatedAt != null ? polylineUpdatedAt.getEpochSecond() : null; + return ThumbnailUrlService.resolveTripThumbnailUrl(id, hasUpdates, tripPlanId, timestamp); } } diff --git a/commons/src/main/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlService.java b/commons/src/main/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlService.java index 901eceed..73954fc7 100644 --- a/commons/src/main/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlService.java +++ b/commons/src/main/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlService.java @@ -38,21 +38,47 @@ public static String generateTripPlanThumbnailUrl(UUID tripPlanId) { /** * Resolves the appropriate thumbnail URL for a trip. If the trip has no updates but has a trip * plan, falls back to the plan thumbnail. Otherwise uses the trip thumbnail. + * Includes a cache-busting query parameter based on the polyline update timestamp. * * @param tripId the trip ID (as String) * @param hasUpdates whether the trip has any updates/locations * @param tripPlanId the trip plan ID (as String), may be null - * @return the resolved thumbnail URL, or null if tripId is null + * @param polylineUpdatedAt the timestamp when polyline was last updated, used for cache busting + * @return the resolved thumbnail URL with cache-busting parameter, or null if tripId is null */ public static String resolveTripThumbnailUrl( - String tripId, boolean hasUpdates, String tripPlanId) { + String tripId, boolean hasUpdates, String tripPlanId, Long polylineUpdatedAt) { if (tripId == null) { return null; } + String baseUrl; if (!hasUpdates && tripPlanId != null && !tripPlanId.isEmpty()) { - return generateTripPlanThumbnailUrl(UUID.fromString(tripPlanId)); + baseUrl = generateTripPlanThumbnailUrl(UUID.fromString(tripPlanId)); + } else { + baseUrl = generateTripThumbnailUrl(UUID.fromString(tripId)); } - return generateTripThumbnailUrl(UUID.fromString(tripId)); + + // Add cache-busting parameter if polylineUpdatedAt is available + if (polylineUpdatedAt != null && polylineUpdatedAt > 0) { + return baseUrl + "?v=" + polylineUpdatedAt; + } + return baseUrl; + } + + /** + * Resolves the appropriate thumbnail URL for a trip without cache-busting. + * Kept for backward compatibility. + * + * @param tripId the trip ID (as String) + * @param hasUpdates whether the trip has any updates/locations + * @param tripPlanId the trip plan ID (as String), may be null + * @return the resolved thumbnail URL, or null if tripId is null + * @deprecated Use {@link #resolveTripThumbnailUrl(String, boolean, String, Long)} instead + */ + @Deprecated + public static String resolveTripThumbnailUrl( + String tripId, boolean hasUpdates, String tripPlanId) { + return resolveTripThumbnailUrl(tripId, hasUpdates, tripPlanId, null); } public static String generateUserProfileThumbnailUrl(UUID userId) { diff --git a/commons/src/test/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlServiceTest.java b/commons/src/test/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlServiceTest.java index 1bb7cac4..8d2ec57c 100644 --- a/commons/src/test/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlServiceTest.java +++ b/commons/src/test/java/com/tomassirio/wanderer/commons/service/ThumbnailUrlServiceTest.java @@ -94,7 +94,7 @@ void generateUserProfileThumbnailUrl_shouldReturnNull_whenNull() { @Test void resolveTripThumbnailUrl_shouldReturnNull_whenTripIdIsNull() { - assertNull(ThumbnailUrlService.resolveTripThumbnailUrl(null, true, null)); + assertNull(ThumbnailUrlService.resolveTripThumbnailUrl(null, true, null, null)); } @Test @@ -104,7 +104,7 @@ void resolveTripThumbnailUrl_shouldReturnTripThumbnail_whenHasUpdates() { String result = ThumbnailUrlService.resolveTripThumbnailUrl( - tripId.toString(), true, planId.toString()); + tripId.toString(), true, planId.toString(), null); assertEquals("/thumbnails/trips/" + tripId + ".png", result); } @@ -116,7 +116,7 @@ void resolveTripThumbnailUrl_shouldReturnPlanThumbnail_whenNoUpdatesAndHasPlan() String result = ThumbnailUrlService.resolveTripThumbnailUrl( - tripId.toString(), false, planId.toString()); + tripId.toString(), false, planId.toString(), null); assertEquals("/thumbnails/plans/" + planId + ".png", result); } @@ -126,7 +126,7 @@ void resolveTripThumbnailUrl_shouldReturnTripThumbnail_whenNoUpdatesAndNoPlan() UUID tripId = UUID.randomUUID(); String result = - ThumbnailUrlService.resolveTripThumbnailUrl(tripId.toString(), false, null); + ThumbnailUrlService.resolveTripThumbnailUrl(tripId.toString(), false, null, null); assertEquals("/thumbnails/trips/" + tripId + ".png", result); } @@ -135,7 +135,7 @@ void resolveTripThumbnailUrl_shouldReturnTripThumbnail_whenNoUpdatesAndNoPlan() void resolveTripThumbnailUrl_shouldReturnTripThumbnail_whenNoUpdatesAndEmptyPlanId() { UUID tripId = UUID.randomUUID(); - String result = ThumbnailUrlService.resolveTripThumbnailUrl(tripId.toString(), false, ""); + String result = ThumbnailUrlService.resolveTripThumbnailUrl(tripId.toString(), false, "", null); assertEquals("/thumbnails/trips/" + tripId + ".png", result); } @@ -145,7 +145,7 @@ void resolveTripThumbnailUrl_shouldReturnTripThumbnail_whenHasUpdatesAndNoPlan() UUID tripId = UUID.randomUUID(); String result = - ThumbnailUrlService.resolveTripThumbnailUrl(tripId.toString(), true, null); + ThumbnailUrlService.resolveTripThumbnailUrl(tripId.toString(), true, null, null); assertEquals("/thumbnails/trips/" + tripId + ".png", result); } @@ -158,9 +158,56 @@ void resolveTripThumbnailUrl_shouldPreferTripThumbnail_whenHasUpdatesEvenWithPla // Even though a plan exists, having updates means we use the trip thumbnail String result = ThumbnailUrlService.resolveTripThumbnailUrl( - tripId.toString(), true, planId.toString()); + tripId.toString(), true, planId.toString(), null); assertEquals("/thumbnails/trips/" + tripId + ".png", result); } + + @Test + void resolveTripThumbnailUrl_shouldIncludeCacheBustingParameter_whenTimestampProvided() { + UUID tripId = UUID.randomUUID(); + Long timestamp = 1234567890L; + + String result = + ThumbnailUrlService.resolveTripThumbnailUrl( + tripId.toString(), true, null, timestamp); + + assertEquals("/thumbnails/trips/" + tripId + ".png?v=" + timestamp, result); + } + + @Test + void resolveTripThumbnailUrl_shouldNotIncludeCacheBustingParameter_whenTimestampIsNull() { + UUID tripId = UUID.randomUUID(); + + String result = + ThumbnailUrlService.resolveTripThumbnailUrl( + tripId.toString(), true, null, null); + + assertEquals("/thumbnails/trips/" + tripId + ".png", result); + } + + @Test + void resolveTripThumbnailUrl_shouldNotIncludeCacheBustingParameter_whenTimestampIsZero() { + UUID tripId = UUID.randomUUID(); + + String result = + ThumbnailUrlService.resolveTripThumbnailUrl( + tripId.toString(), true, null, 0L); + + assertEquals("/thumbnails/trips/" + tripId + ".png", result); + } + + @Test + void resolveTripThumbnailUrl_shouldIncludeCacheBustingOnPlanThumbnail_whenTimestampProvided() { + UUID tripId = UUID.randomUUID(); + UUID planId = UUID.randomUUID(); + Long timestamp = 9876543210L; + + String result = + ThumbnailUrlService.resolveTripThumbnailUrl( + tripId.toString(), false, planId.toString(), timestamp); + + assertEquals("/thumbnails/plans/" + planId + ".png?v=" + timestamp, result); + } } diff --git a/wanderer-query/src/main/java/com/tomassirio/wanderer/query/service/helper/TripEnrichmentHelper.java b/wanderer-query/src/main/java/com/tomassirio/wanderer/query/service/helper/TripEnrichmentHelper.java index eec88a97..2fe145a4 100644 --- a/wanderer-query/src/main/java/com/tomassirio/wanderer/query/service/helper/TripEnrichmentHelper.java +++ b/wanderer-query/src/main/java/com/tomassirio/wanderer/query/service/helper/TripEnrichmentHelper.java @@ -182,7 +182,8 @@ public static TripSummaryDTO toTripSummaryDTO( isPromoted, promotedInfo != null ? promotedInfo.getPromotedAt() : null, promotedInfo != null && promotedInfo.isPreAnnounced(), - promotedInfo != null ? promotedInfo.getCountdownStartDate() : null); + promotedInfo != null ? promotedInfo.getCountdownStartDate() : null, + trip.getPolylineUpdatedAt()); // For cache-busting } /** diff --git a/wanderer-query/src/test/java/com/tomassirio/wanderer/query/controller/TripControllerTest.java b/wanderer-query/src/test/java/com/tomassirio/wanderer/query/controller/TripControllerTest.java index b11d18ff..e864e0e9 100644 --- a/wanderer-query/src/test/java/com/tomassirio/wanderer/query/controller/TripControllerTest.java +++ b/wanderer-query/src/test/java/com/tomassirio/wanderer/query/controller/TripControllerTest.java @@ -639,6 +639,7 @@ private TripSummaryDTO createTripSummaryDTOWithStatus( Boolean.FALSE, // isPromoted null, // promotedAt Boolean.FALSE, // isPreAnnounced - null); // countdownStartDate + null, // countdownStartDate + null); // polylineUpdatedAt } } diff --git a/wanderer-query/src/test/java/com/tomassirio/wanderer/query/service/impl/SearchServiceImplTest.java b/wanderer-query/src/test/java/com/tomassirio/wanderer/query/service/impl/SearchServiceImplTest.java index d10deec7..e50beb95 100644 --- a/wanderer-query/src/test/java/com/tomassirio/wanderer/query/service/impl/SearchServiceImplTest.java +++ b/wanderer-query/src/test/java/com/tomassirio/wanderer/query/service/impl/SearchServiceImplTest.java @@ -499,7 +499,8 @@ private TripSummaryDTO createTripSummaryDTO(UUID tripId, UUID userId, String nam false, // isPromoted null, // promotedAt false, // isPreAnnounced - null); // countdownStartDate + null, // countdownStartDate + null); // polylineUpdatedAt } private TripSummaryDTO createPromotedTripSummaryDTO( @@ -518,7 +519,8 @@ private TripSummaryDTO createPromotedTripSummaryDTO( true, // isPromoted promotedAt, false, // isPreAnnounced - null); // countdownStartDate + null, // countdownStartDate + null); // polylineUpdatedAt } private UserSummaryDto createUserSummary(UUID id, String username, String displayName) {