From 8535ce44eb2c6a95519f3289421dc98472a51747 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 16 Dec 2024 17:19:44 +0100 Subject: [PATCH] bug6077: first implementation of REST API end point for competitordata --- .../gateway/jaxrs/api/RegattasResource.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java index db7ab7c3c74..f2cd97819fa 100755 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java @@ -17,7 +17,10 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ScheduledExecutorService; import java.util.logging.Level; import java.util.logging.Logger; @@ -76,6 +79,7 @@ import com.sap.sailing.domain.base.impl.DynamicBoat; import com.sap.sailing.domain.base.impl.PersonImpl; import com.sap.sailing.domain.base.impl.TeamImpl; import com.sap.sailing.domain.common.CompetitorRegistrationType; +import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.DeviceIdentifier; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.NoWindException; @@ -100,6 +104,7 @@ import com.sap.sailing.domain.common.tracking.GPSFix; import com.sap.sailing.domain.common.tracking.GPSFixMoving; import com.sap.sailing.domain.common.tracking.impl.CompetitorJsonConstants; import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; import com.sap.sailing.domain.racelogtracking.DeviceMappingWithRegattaLogEvent; @@ -194,6 +199,7 @@ import com.sap.sse.shared.json.JsonDeserializationException; import com.sap.sse.shared.json.JsonSerializer; import com.sap.sse.shared.util.impl.UUIDHelper; import com.sap.sse.util.SingleCalculationPerSubjectCache; +import com.sap.sse.util.ThreadPoolUtil; @Path("/v1/regattas") public class RegattasResource extends AbstractSailingServerResource { @@ -203,6 +209,8 @@ public class RegattasResource extends AbstractSailingServerResource { private DataMiningResource dataMiningResource; + private final ScheduledExecutorService executor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor(); + private class CompetitorRanksRequest { private final Regatta regatta; private final RaceColumn raceColumn; @@ -2122,6 +2130,149 @@ public class RegattasResource extends AbstractSailingServerResource { } return response; } + + @GET + @Produces("application/json;charset=UTF-8") + @Path("{regattaname}/races/{racename}/competitordata") + public Response getCompetitorRaceData( + @PathParam("regattaname") final String regattaName, + @PathParam("racename") final String raceName, + @QueryParam("fromtime") final String fromtime, @QueryParam("fromtimeasmillis") Long fromtimeasmillis, + @QueryParam("totime") final String totime, @QueryParam("totimeasmillis") Long totimeasmillis, + @QueryParam("competitorId") final Set competitorIds, + @QueryParam("detailType") final Set detailTypes, + @QueryParam("leaderboardGroupNameOrUUID") final String leaderboardGroupName, + @QueryParam("leaderboardName") final String leaderboardName, + @QueryParam("stepSizeMillis") final long stepSizeMillis) { + final Response response; + final ConcurrentHashMap cachesByTimePoint = new ConcurrentHashMap<>(); + final TimePoint from; + final TimePoint to; + final Regatta regatta = findRegattaByName(regattaName); + final TrackedRace trackedRace; + final Serializable uuid = UUIDHelper.tryUuidConversion(leaderboardGroupName); + final LeaderboardGroup leaderboardGroup; + if (uuid != leaderboardGroupName) { + leaderboardGroup = getService().getLeaderboardGroupByID((UUID) uuid); + } else { + leaderboardGroup = getService().getLeaderboardGroupByName(leaderboardGroupName); + } + if (leaderboardGroup == null) { + response = Response.status(Status.NOT_FOUND) + .entity("Could not find a leaderboard group with name '" + + StringEscapeUtils.escapeHtml(leaderboardGroupName) + "'.") + .type(MediaType.TEXT_PLAIN).build(); + } else { + if (regatta == null) { + return getBadRegattaErrorResponse(regattaName); + } else { + RaceDefinition race = findRaceByName(regatta, raceName); + if (race == null) { + return getBadRaceErrorResponse(regattaName, raceName); + } else { + trackedRace = findTrackedRace(regattaName, raceName); + if (trackedRace == null) { + return getBadRaceErrorResponse(regattaName, raceName); + } + getSecurityService().checkCurrentUserReadPermission(trackedRace); + try { + from = parseTimePoint(fromtime, fromtimeasmillis, + trackedRace.getStartOfRace() == null ? new MillisecondsTimePoint(0) : + /* 24h before race start */new MillisecondsTimePoint(trackedRace.getStartOfRace() + .asMillis() - 24 * 3600 * 1000)); + } catch (InvalidDateException e1) { + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Could not parse the 'from' time.") + .type(MediaType.TEXT_PLAIN).build(); + } + try { + to = parseTimePoint(totime, totimeasmillis, MillisecondsTimePoint.now()); + } catch (InvalidDateException e1) { + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Could not parse the 'to' time.") + .type(MediaType.TEXT_PLAIN).build(); + } + } + } + final int MAX_NUMBER_OF_FIXES_TO_QUERY = 10000; // TODO harmonize with SailingServiceConstants.MAX_NUMBER_OF_FIXES_TO_QUERY? + final Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); + final TimePoint newestEvent = trackedRace.getTimePointOfNewestEvent(); + final TimePoint startTime = from == null ? trackedRace.getStartOfTracking() : from; + final TimePoint endTime = (to == null || to.after(newestEvent)) ? newestEvent : to; + final long adjustedStepSizeInMillis = (long) Math.max((double) stepSizeMillis, startTime.until(endTime).divide(MAX_NUMBER_OF_FIXES_TO_QUERY).asMillis()); + final int MAX_CACHE_SIZE = MAX_NUMBER_OF_FIXES_TO_QUERY; + for (final DetailType detailType : detailTypes) { + if (detailType.getPremiumAction() != null && leaderboard.getPermissionType().supports(detailType.getPremiumAction())) { + getSecurityService().checkCurrentUserExplicitPermissions(leaderboard, detailType.getPremiumAction()); + } + } + final Map>>>> resultFutures = new HashMap<>(); + for (final String competitorIdAsString : competitorIds) { + final Competitor competitor = getService().getCompetitorAndBoatStore().getExistingCompetitorByIdAsString(competitorIdAsString); + if (competitor == null) { + return getBadCompetitorIdResponse(competitorIdAsString); + } + getSecurityService().checkCurrentUserExplicitPermissions(competitor, SecuredSecurityTypes.PublicReadableActions.READ_PUBLIC); + final FutureTask>>> future =new FutureTask>>>( + new Callable>>>() { + @Override + public Iterable>> call() throws NoWindException { + final List>> raceData = new ArrayList<>(); + if (startTime != null && endTime != null) { + for (long i = startTime.asMillis(); i <= endTime.asMillis(); i += adjustedStepSizeInMillis) { + final TimePoint time = TimePoint.of(i); + WindLegTypeAndLegBearingAndORCPerformanceCurveCache cache = cachesByTimePoint.get(time); + if (cache == null) { + cache = new LeaderboardDTOCalculationReuseCache(time); + // ensure maximum number of cache objects: + if (cachesByTimePoint.size() >= MAX_CACHE_SIZE) { + final Iterator> iterator = cachesByTimePoint.entrySet().iterator(); + while (cachesByTimePoint.size() >= MAX_CACHE_SIZE && iterator.hasNext()) { + iterator.next(); + iterator.remove(); + } + } + cachesByTimePoint.put(time, cache); + } + final Map valuesForTimepoint = new HashMap<>(); + raceData.add(new Pair<>(time, valuesForTimepoint)); + for (final DetailType detailType : detailTypes) { + final Double competitorRaceData = getService() + .getCompetitorRaceDataEntry(detailType, trackedRace, competitor, + time, leaderboardGroup, leaderboardName, cache); + if (competitorRaceData != null) { + valuesForTimepoint.put(detailType, competitorRaceData); + } + } + } + } + return raceData; + } + }); + resultFutures.put(competitor, future); + executor.execute(future); // security checks happen before; no need to associate future with Subject/session + } + final JSONObject result = new JSONObject(); + for (Entry>>>> e : resultFutures.entrySet()) { + try { + final JSONArray resultsForCompetitor = new JSONArray(); + result.put(e.getKey().getId().toString(), resultsForCompetitor); + for (final Pair> competitorData : e.getValue().get()) { + final JSONObject resultsForCompetitorAtTimepoint = new JSONObject(); + resultsForCompetitorAtTimepoint.put("timepoint-ms", competitorData.getA().asMillis()); + final JSONObject resultsForCompetitorByDetailType = new JSONObject(); + for (final Entry entry : competitorData.getB().entrySet()) { + resultsForCompetitorByDetailType.put(entry.getKey().name(), entry.getValue()); + } + resultsForCompetitorAtTimepoint.put("values", resultsForCompetitorByDetailType); + resultsForCompetitor.add(resultsForCompetitorAtTimepoint); + } + } catch (InterruptedException | ExecutionException e1) { + logger.log(Level.SEVERE, "Exception while trying to compute competitor data "+detailTypes+" for competitor "+e.getKey().getName(), e1); + } + } + response = Response.ok(streamingOutput(result)).build(); + } + return response; + } private Integer getNullableValueFromDefault(Integer value) { return Integer.MIN_VALUE == value ? null : value;