bug6077: first implementation of REST API end point for competitordata

This commit is contained in:
Axel Uhl
2024-12-16 17:19:44 +01:00
parent 6270b3911b
commit 8535ce44eb
@@ -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<String> competitorIds,
@QueryParam("detailType") final Set<DetailType> detailTypes,
@QueryParam("leaderboardGroupNameOrUUID") final String leaderboardGroupName,
@QueryParam("leaderboardName") final String leaderboardName,
@QueryParam("stepSizeMillis") final long stepSizeMillis) {
final Response response;
final ConcurrentHashMap<TimePoint, WindLegTypeAndLegBearingAndORCPerformanceCurveCache> 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<Competitor, FutureTask<Iterable<Pair<TimePoint, Map<DetailType, Double>>>>> 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<Iterable<Pair<TimePoint, Map<DetailType, Double>>>> future =new FutureTask<Iterable<Pair<TimePoint, Map<DetailType, Double>>>>(
new Callable<Iterable<Pair<TimePoint, Map<DetailType, Double>>>>() {
@Override
public Iterable<Pair<TimePoint, Map<DetailType, Double>>> call() throws NoWindException {
final List<Pair<TimePoint, Map<DetailType, Double>>> 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<Entry<TimePoint, WindLegTypeAndLegBearingAndORCPerformanceCurveCache>> iterator = cachesByTimePoint.entrySet().iterator();
while (cachesByTimePoint.size() >= MAX_CACHE_SIZE && iterator.hasNext()) {
iterator.next();
iterator.remove();
}
}
cachesByTimePoint.put(time, cache);
}
final Map<DetailType, Double> 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<Competitor, FutureTask<Iterable<Pair<TimePoint, Map<DetailType, Double>>>>> e : resultFutures.entrySet()) {
try {
final JSONArray resultsForCompetitor = new JSONArray();
result.put(e.getKey().getId().toString(), resultsForCompetitor);
for (final Pair<TimePoint, Map<DetailType, Double>> competitorData : e.getValue().get()) {
final JSONObject resultsForCompetitorAtTimepoint = new JSONObject();
resultsForCompetitorAtTimepoint.put("timepoint-ms", competitorData.getA().asMillis());
final JSONObject resultsForCompetitorByDetailType = new JSONObject();
for (final Entry<DetailType, Double> 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;