mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-22 20:13:52 +00:00
wired user notifications to a new LeaderboardScoreCorrectionNotifier in RacingEventService; wired notifications with TrackedRaceReplicatorAndNotifier
Change-Id: I4ff77cf49700140b134dc95326a3f7a844958d95
This commit is contained in:
1 parent
17eed109d1
commit
d6419c51cb
18 files changed
+346
-93
No files matched your search
@@ -3,9 +3,9 @@ raceInFleetInRegattaOfEvent={0} ({3}) in regatta {1} of event {2}
|
||||
|
||||
leaderboardOfEvent=leaderboard {0} of event {1}
|
||||
|
||||
boatClassRaceFinishingSubject=Race for boat class {0} finishes - new results available
|
||||
boatClassRaceFinishedSubject=Race for boat class {0} has finished - new results available
|
||||
# 0: boatClassName, 1: raceDescription, 2: link
|
||||
boatClassRaceFinishingBody=New results for boat class {0} in {1} are available. See: {2}
|
||||
boatClassRaceFinishedBody=New results for boat class {0} in {1} are available. See: {2}
|
||||
|
||||
boatClassScoreCorrectionSubject=New results are available for boat class {0}
|
||||
# 0: boatClassName, 1: leaderboardDescription, 2: link
|
||||
|
||||
@@ -100,7 +100,7 @@ public class Activator implements BundleActivator {
|
||||
// instead.
|
||||
serviceFinderFactory = new CachedOsgiTypeBasedServiceFinderFactory(context);
|
||||
racingEventService = new RacingEventServiceImpl(clearPersistentCompetitors, serviceFinderFactory,
|
||||
trackedRegattaListener);
|
||||
trackedRegattaListener, notificationService);
|
||||
notificationService.setRacingEventService(racingEventService);
|
||||
|
||||
masterDataImportClassLoaderServiceTracker = new ServiceTracker<MasterDataImportClassLoaderService, MasterDataImportClassLoaderService>(
|
||||
|
||||
+11
-4
@@ -18,21 +18,28 @@ import com.sap.sailing.server.operationaltransformation.RecordRaceLogEventOnLead
|
||||
import com.sap.sailing.server.operationaltransformation.RecordRaceLogEventOnRegatta;
|
||||
|
||||
/**
|
||||
* Being a {@link RaceColumnListener}, this replicator must be added to all {@link RaceColumn}s managing a
|
||||
* Being a {@link RaceColumnListener}, this replicator and notifier must be added to all {@link RaceColumn}s managing a
|
||||
* {@link RaceLog} so as to be notified about changes to the race log. This largely happens by callbacks to the
|
||||
* {@link #raceLogEventAdded(RaceColumn, RaceLogIdentifier, RaceLogEvent)} method. This object will then use the
|
||||
* {@link Replicator} passed to this object's constructor and send a {@link RecordRaceLogEventOnLeaderboard} or a
|
||||
* {@link RecordRaceLogEventOnRegatta} operation to all replicas.
|
||||
* {@link RecordRaceLogEventOnRegatta} operation to all replicas.<p>
|
||||
*
|
||||
* Since all {@link RaceColumn}s can manage a {@link RaceLog}, this object is registered on all relevant
|
||||
* {@link RaceColumn}s. This also lets this object conveniently being informed about all other events that
|
||||
* a {@link RaceColumnListener} is notified about, some of which contain interesting information regarding
|
||||
* the notifying of users interested in certain types of changes. For example, when a {@link TrackedRace}
|
||||
* {@link #trackedRaceLinked(RaceColumn, Fleet, TrackedRace) is linked} to a race column, this may be of
|
||||
* interest to some users.
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
public class RaceLogReplicator implements RaceColumnListener {
|
||||
public class RaceLogReplicatorAndNotifier implements RaceColumnListener {
|
||||
private static final long serialVersionUID = 7190510926643574068L;
|
||||
|
||||
private final Replicator service;
|
||||
|
||||
public RaceLogReplicator(Replicator service) {
|
||||
public RaceLogReplicatorAndNotifier(Replicator service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
+233
-38
@@ -95,6 +95,7 @@ import com.sap.sailing.domain.base.impl.RemoteSailingServerReferenceImpl;
|
||||
import com.sap.sailing.domain.common.DataImportProgress;
|
||||
import com.sap.sailing.domain.common.DataImportSubProgress;
|
||||
import com.sap.sailing.domain.common.Distance;
|
||||
import com.sap.sailing.domain.common.MaxPointsReason;
|
||||
import com.sap.sailing.domain.common.Position;
|
||||
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
||||
import com.sap.sailing.domain.common.RegattaIdentifier;
|
||||
@@ -119,7 +120,9 @@ import com.sap.sailing.domain.leaderboard.Leaderboard;
|
||||
import com.sap.sailing.domain.leaderboard.LeaderboardGroup;
|
||||
import com.sap.sailing.domain.leaderboard.LeaderboardRegistry;
|
||||
import com.sap.sailing.domain.leaderboard.RegattaLeaderboard;
|
||||
import com.sap.sailing.domain.leaderboard.ScoreCorrectionListener;
|
||||
import com.sap.sailing.domain.leaderboard.ScoringScheme;
|
||||
import com.sap.sailing.domain.leaderboard.SettableScoreCorrection;
|
||||
import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl;
|
||||
import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl;
|
||||
import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl;
|
||||
@@ -177,6 +180,7 @@ import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJ
|
||||
import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResultBaseJsonDeserializer;
|
||||
import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer;
|
||||
import com.sap.sailing.server.masterdata.DataImportLockWithProgress;
|
||||
import com.sap.sailing.server.notification.SailingNotificationService;
|
||||
import com.sap.sailing.server.operationaltransformation.AddCourseAreas;
|
||||
import com.sap.sailing.server.operationaltransformation.AddDefaultRegatta;
|
||||
import com.sap.sailing.server.operationaltransformation.AddMediaTrackOperation;
|
||||
@@ -220,6 +224,7 @@ import com.sap.sailing.server.operationaltransformation.UpdateWindSourcesToExclu
|
||||
import com.sap.sailing.server.simulation.SimulationService;
|
||||
import com.sap.sailing.server.simulation.SimulationServiceFactory;
|
||||
import com.sap.sse.ServerInfo;
|
||||
import com.sap.sse.common.Duration;
|
||||
import com.sap.sse.common.TimePoint;
|
||||
import com.sap.sse.common.TypeBasedServiceFinderFactory;
|
||||
import com.sap.sse.common.Util;
|
||||
@@ -362,7 +367,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Regatta> persistentRegattasForRaceIDs;
|
||||
|
||||
private final RaceLogReplicator raceLogReplicator;
|
||||
private final RaceLogReplicatorAndNotifier raceLogReplicator;
|
||||
private final RegattaLogReplicator regattaLogReplicator;
|
||||
|
||||
private final RaceLogScoringReplicator raceLogScoringReplicator;
|
||||
@@ -390,6 +395,18 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
|
||||
private final SimulationService simulationService;
|
||||
|
||||
/**
|
||||
* A service that, if not {@code null}, must be called when certain events that the service can notify users about
|
||||
* have occurred. For example, the
|
||||
* {@link SailingNotificationService#notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(com.sap.sailing.domain.base.BoatClass, Leaderboard)}
|
||||
* method must be called whenever a new set of score corrections have been made available.<p>
|
||||
*
|
||||
* The field is transient because we don't want the service to be serialized from a master to a replica.
|
||||
* Replicas are expected to not notify users about anything, particularly because they usually don't
|
||||
* have a valid mail service, either.
|
||||
*/
|
||||
private transient final SailingNotificationService notificationService;
|
||||
|
||||
/**
|
||||
* Allow only one master data import at a time to avoid situation where multiple Imports override each other in
|
||||
* unpredictable fashion
|
||||
@@ -422,6 +439,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
private SailingServerConfiguration sailingServerConfiguration;
|
||||
|
||||
private final TrackedRegattaListener trackedRegattaListener;
|
||||
|
||||
private transient ConcurrentHashMap<Leaderboard, ScoreCorrectionListener> scoreCorrectionListenersByLeaderboard;
|
||||
|
||||
/**
|
||||
* Providing the constructor parameters for a new {@link RacingEventServiceImpl} instance is a bit tricky
|
||||
@@ -453,7 +472,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
|
||||
public RacingEventServiceImpl(WindStore windStore, SensorFixStore sensorFixStore,
|
||||
TypeBasedServiceFinderFactory serviceFinderFactory) {
|
||||
this(true, windStore, sensorFixStore, serviceFinderFactory);
|
||||
this(true, windStore, sensorFixStore, serviceFinderFactory, /* sailingNotificationService */ null);
|
||||
}
|
||||
|
||||
void setBundleContext(BundleContext bundleContext) {
|
||||
@@ -461,7 +480,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
}
|
||||
|
||||
public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory) {
|
||||
this(clearPersistentCompetitorStore, serviceFinderFactory, null);
|
||||
this(clearPersistentCompetitorStore, serviceFinderFactory, null, /* sailingNotificationService */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -472,8 +491,12 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
* if <code>true</code>, the {@link PersistentCompetitorStore} is created empty, with the corresponding
|
||||
* database collection cleared as well. Use with caution! When used with <code>false</code>, competitors
|
||||
* created and stored during previous service executions will initially be loaded.
|
||||
* @param sailingNotificationService
|
||||
* a notification service to call upon events worth notifying users about, or {@code null} if no
|
||||
* notification service is available, e.g., in test set-ups
|
||||
*/
|
||||
public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory, TrackedRegattaListener trackedRegattaListener) {
|
||||
public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory,
|
||||
TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService) {
|
||||
this((final RaceLogResolver raceLogResolver)-> {
|
||||
return new ConstructorParameters() {
|
||||
private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory);
|
||||
@@ -486,11 +509,11 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
@Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); }
|
||||
@Override public CompetitorStore getCompetitorStore() { return competitorStore; }
|
||||
};
|
||||
}, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory ,trackedRegattaListener);
|
||||
}, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory ,trackedRegattaListener, sailingNotificationService);
|
||||
}
|
||||
|
||||
private RacingEventServiceImpl(final boolean clearPersistentCompetitorStore, WindStore windStore,
|
||||
SensorFixStore sensorFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory) {
|
||||
SensorFixStore sensorFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory, SailingNotificationService sailingNotificationService) {
|
||||
this((final RaceLogResolver raceLogResolver)-> {
|
||||
return new ConstructorParameters() {
|
||||
private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory);
|
||||
@@ -503,29 +526,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
@Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); }
|
||||
@Override public CompetitorStore getCompetitorStore() { return competitorStore; }
|
||||
};
|
||||
}, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, sensorFixStore, serviceFinderFactory, null);
|
||||
}, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, sensorFixStore, serviceFinderFactory, null, sailingNotificationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the default factories for the tracking adapters and the {@link DomainFactory base domain factory} of the
|
||||
* {@link PersistenceFactory#getDefaultDomainObjectFactory() default domain object factory}. This constructor should
|
||||
* be used for testing because it provides a transient {@link CompetitorStore} as required for competitor
|
||||
* persistence.
|
||||
*/
|
||||
public RacingEventServiceImpl(Function<RaceLogResolver, DomainObjectFactory> domainObjectFactoryProvider,
|
||||
final MongoObjectFactory mongoObjectFactory, MediaDB mediaDB, WindStore windStore, SensorFixStore sensorFixStore) {
|
||||
this((final RaceLogResolver raceLogResolver)-> {
|
||||
return new ConstructorParameters() {
|
||||
private final DomainObjectFactory domainObjectFactory = domainObjectFactoryProvider.apply(raceLogResolver);
|
||||
|
||||
@Override public DomainObjectFactory getDomainObjectFactory() { return domainObjectFactory; }
|
||||
@Override public MongoObjectFactory getMongoObjectFactory() { return mongoObjectFactory; }
|
||||
@Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); }
|
||||
@Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); }
|
||||
};
|
||||
}, mediaDB, windStore, sensorFixStore, null, null);
|
||||
}
|
||||
|
||||
public RacingEventServiceImpl(final DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory,
|
||||
MediaDB mediaDB, WindStore windStore, SensorFixStore sensorFixStore) {
|
||||
this((final RaceLogResolver raceLogResolver)-> {
|
||||
@@ -535,7 +538,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
@Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); }
|
||||
@Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); }
|
||||
};
|
||||
}, mediaDB, windStore, sensorFixStore, null, null);
|
||||
}, mediaDB, windStore, sensorFixStore, null, null, /* sailingNotificationService */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,10 +549,15 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
* used to find the services handling specific types of tracking devices, such as the persistent storage
|
||||
* of {@link DeviceIdentifier}s of specific device types or the managing of the device-to-competitor
|
||||
* associations per race tracked.
|
||||
* @param sailingNotificationService
|
||||
* a notification service to call upon events worth notifying users about, or {@code null} if no
|
||||
* notification service is available, e.g., in test set-ups
|
||||
*/
|
||||
public RacingEventServiceImpl(Function<RaceLogResolver, ConstructorParameters> constructorParametersProvider, MediaDB mediaDb,
|
||||
final WindStore windStore, final SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory, TrackedRegattaListener trackedRegattaListener) {
|
||||
final WindStore windStore, final SensorFixStore sensorFixStore, TypeBasedServiceFinderFactory serviceFinderFactory,
|
||||
TrackedRegattaListener trackedRegattaListener, SailingNotificationService sailingNotificationService) {
|
||||
logger.info("Created " + this);
|
||||
this.notificationService = sailingNotificationService;
|
||||
final ConstructorParameters constructorParameters = constructorParametersProvider.apply(this);
|
||||
this.domainObjectFactory = constructorParameters.getDomainObjectFactory();
|
||||
this.masterDataClassLoaders.add(this.getClass().getClassLoader());
|
||||
@@ -606,7 +614,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
Thread.NORM_PRIORITY, /* daemon */true));
|
||||
// TODO: initialize smart-future-cache for simulation-results and add to simulation-service
|
||||
simulationService = SimulationServiceFactory.INSTANCE.getService(simulatorExecutor, this);
|
||||
this.raceLogReplicator = new RaceLogReplicator(this);
|
||||
this.raceLogReplicator = new RaceLogReplicatorAndNotifier(this);
|
||||
this.regattaLogReplicator = new RegattaLogReplicator(this);
|
||||
this.raceLogScoringReplicator = new RaceLogScoringReplicator(this);
|
||||
this.mediaLibrary = new MediaLibrary();
|
||||
@@ -777,6 +785,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
leaderboard.addRaceColumnListener(raceLogReplicator);
|
||||
leaderboard.addRaceColumnListener(raceLogScoringReplicator);
|
||||
}
|
||||
final LeaderboardScoreCorrectionNotifier scoreCorrectionListener = new LeaderboardScoreCorrectionNotifier(leaderboard);
|
||||
scoreCorrectionListenersByLeaderboard.put(leaderboard, scoreCorrectionListener);
|
||||
leaderboard.getScoreCorrection().addScoreCorrectionListener(scoreCorrectionListener);
|
||||
}
|
||||
|
||||
private void loadStoredLeaderboardsAndGroups() {
|
||||
@@ -1008,6 +1019,10 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
if (leaderboard != null) {
|
||||
leaderboard.removeRaceColumnListener(raceLogReplicator);
|
||||
leaderboard.removeRaceColumnListener(raceLogScoringReplicator);
|
||||
final ScoreCorrectionListener scoreCorrectionListener = scoreCorrectionListenersByLeaderboard.remove(leaderboard);
|
||||
if (scoreCorrectionListener != null) {
|
||||
leaderboard.getScoreCorrection().removeScoreCorrectionListener(scoreCorrectionListener);
|
||||
}
|
||||
mongoObjectFactory.removeLeaderboard(leaderboardName);
|
||||
syncGroupsAfterLeaderboardRemove(leaderboardName, true);
|
||||
if (leaderboard instanceof FlexibleLeaderboard) {
|
||||
@@ -1537,7 +1552,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
* service, the service adds the tracked race to the default leaderboard and links it to the leaderboard columns
|
||||
* that were previously connected to it. Additionally, a {@link RaceChangeListener} is added to the
|
||||
* {@link TrackedRace} which is responsible for triggering the replication of all relevant changes to the tracked
|
||||
* race. When a tracked race is removed, the {@link TrackedRaceReplicator} that was added as listener to that
|
||||
* race. When a tracked race is removed, the {@link TrackedRaceReplicatorAndNotifier} that was added as listener to that
|
||||
* tracked race is removed again.
|
||||
*
|
||||
* A {@link PolarFixCacheUpdater} is added to every race so that polar fixes are aggregated when new GPS fixes
|
||||
@@ -1549,18 +1564,18 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
private class RaceAdditionListener implements RaceListener, Serializable {
|
||||
private static final long serialVersionUID = 1036955460477000265L;
|
||||
|
||||
private final Map<TrackedRace, TrackedRaceReplicator> trackedRaceReplicators;
|
||||
private final Map<TrackedRace, TrackedRaceReplicatorAndNotifier> trackedRaceReplicators;
|
||||
|
||||
private final Map<TrackedRace, PolarFixCacheUpdater> polarFixCacheUpdaters;
|
||||
|
||||
public RaceAdditionListener() {
|
||||
this.trackedRaceReplicators = new HashMap<TrackedRace, TrackedRaceReplicator>();
|
||||
this.trackedRaceReplicators = new HashMap<TrackedRace, TrackedRaceReplicatorAndNotifier>();
|
||||
this.polarFixCacheUpdaters = new HashMap<TrackedRace, PolarFixCacheUpdater>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void raceRemoved(TrackedRace trackedRace) {
|
||||
TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace);
|
||||
TrackedRaceReplicatorAndNotifier trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace);
|
||||
if (trackedRaceReplicator != null) {
|
||||
trackedRace.removeListener(trackedRaceReplicator);
|
||||
}
|
||||
@@ -1578,7 +1593,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
trackedRace.getMillisecondsOverWhichToAverageSpeed());
|
||||
replicate(op);
|
||||
linkRaceToConfiguredLeaderboardColumns(trackedRace);
|
||||
TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace);
|
||||
TrackedRaceReplicatorAndNotifier trackedRaceReplicator = new TrackedRaceReplicatorAndNotifier(trackedRace);
|
||||
trackedRaceReplicators.put(trackedRace, trackedRaceReplicator);
|
||||
trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, true);
|
||||
|
||||
@@ -1591,6 +1606,98 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A score correction listener for a leaderboard that notifies interested users through the
|
||||
* {@link RacingEventServiceImpl#notificationService} if one is available.
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
private class LeaderboardScoreCorrectionNotifier implements ScoreCorrectionListener {
|
||||
/**
|
||||
* We don't want to flood the users with notifications about what's basically caused by the
|
||||
* same original event. For example, when many single score correction updates are applied to
|
||||
* the same leaderboard, we don't want to notify users for each such change if they are generally
|
||||
* interested in new results for that leaderboard or this boat class.
|
||||
*/
|
||||
private final Duration HOW_LONG_BETWEEN_TWO_NOTIFICATIONS_FOR_SIMILAR_EVENT = Duration.ONE_MINUTE.times(5);
|
||||
|
||||
private TimePoint lastNotificationForLeaderboard;
|
||||
|
||||
private final ConcurrentHashMap<Competitor, TimePoint> lastNotificationForCompetitor;
|
||||
|
||||
private final Leaderboard leaderboard;
|
||||
|
||||
/**
|
||||
* Callers are expected to {@link SettableScoreCorrection#addScoreCorrectionListener(ScoreCorrectionListener)
|
||||
* register} this listener as a {@link ScoreCorrectionListener} themselves.
|
||||
*/
|
||||
public LeaderboardScoreCorrectionNotifier(Leaderboard leaderboard) {
|
||||
this.leaderboard = leaderboard;
|
||||
this.lastNotificationForCompetitor = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void correctedScoreChanced(Competitor competitor, RaceColumn raceColumn, Double oldCorrectedScore,
|
||||
Double newCorrectedScore) {
|
||||
notifyForCompetitorIfNotAlreadyNotifiedRecently(competitor, raceColumn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) {
|
||||
notifyForCompetitorIfNotAlreadyNotifiedRecently(competitor, raceColumn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void carriedPointsChanged(Competitor competitor, Double oldCarriedPoints, Double newCarriedPoints) {
|
||||
notifyForCompetitorIfNotAlreadyNotifiedRecently(competitor, /* no raceColumn in case of carried points */ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isSuppressedChanged(Competitor competitor, boolean newIsSuppressed) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void timePointOfLastCorrectionsValidityChanged(TimePoint oldTimePointOfLastCorrectionsValidity,
|
||||
TimePoint newTimePointOfLastCorrectionsValidity) {
|
||||
notifyForLeaderboardIfNotAlreadyNotifiedRecently();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commentChanged(String oldComment, String newComment) {
|
||||
notifyForLeaderboardIfNotAlreadyNotifiedRecently();
|
||||
}
|
||||
|
||||
private void notifyForLeaderboardIfNotAlreadyNotifiedRecently() {
|
||||
final TimePoint now = MillisecondsTimePoint.now();
|
||||
if (notificationService != null && (lastNotificationForLeaderboard == null ||
|
||||
lastNotificationForLeaderboard.until(now).compareTo(HOW_LONG_BETWEEN_TWO_NOTIFICATIONS_FOR_SIMILAR_EVENT) >= 0)) {
|
||||
new Thread(()->notificationService.notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(
|
||||
leaderboard.getBoatClass(), leaderboard),
|
||||
"Score correction notifier for leaderboard "+leaderboard.getName()).start();
|
||||
lastNotificationForLeaderboard = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param raceColumn
|
||||
* may be {@code null} which means that something may have changed for the competitor outside of a
|
||||
* specific race column, such as the carried points
|
||||
*/
|
||||
private void notifyForCompetitorIfNotAlreadyNotifiedRecently(Competitor competitor, RaceColumn raceColumn) {
|
||||
final TimePoint now = MillisecondsTimePoint.now();
|
||||
if (notificationService != null && (!lastNotificationForCompetitor.containsKey(competitor) ||
|
||||
lastNotificationForCompetitor.get(competitor).until(now).compareTo(HOW_LONG_BETWEEN_TWO_NOTIFICATIONS_FOR_SIMILAR_EVENT) >= 0)) {
|
||||
new Thread(()->notificationService.notifyUserOnCompetitorScoreCorrections(competitor, leaderboard),
|
||||
"Score correction notifier for competitor "+competitor.getName()+" in leaderboard "+
|
||||
leaderboard.getName()).start();
|
||||
lastNotificationForCompetitor.put(competitor, now);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class PolarFixCacheUpdater extends AbstractRaceChangeListener {
|
||||
|
||||
@@ -1619,10 +1726,19 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
|
||||
}
|
||||
|
||||
private class TrackedRaceReplicator implements RaceChangeListener {
|
||||
/**
|
||||
* When changes occur on a {@link TrackedRace}, this object will be notified in its role of being a
|
||||
* {@link RaceChangeListener}. It does two things: replicate the changes to replica servers and, potentially, if
|
||||
* this is a replica, back to a master; notify users who expressed a corresponding interest about the change if we
|
||||
* have a {@link RacingEventServiceImpl##notificationService} available.
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
private class TrackedRaceReplicatorAndNotifier implements RaceChangeListener {
|
||||
private final TrackedRace trackedRace;
|
||||
|
||||
public TrackedRaceReplicator(TrackedRace trackedRace) {
|
||||
public TrackedRaceReplicatorAndNotifier(TrackedRace trackedRace) {
|
||||
this.trackedRace = trackedRace;
|
||||
}
|
||||
|
||||
@@ -1648,14 +1764,28 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
|
||||
@Override
|
||||
public void startOfRaceChanged(TimePoint oldStartOfRace, TimePoint newStartOfRace) {
|
||||
// no action required; the update signaled by this call is implicit; for explicit updates
|
||||
// no replication action required; the update signaled by this call is implicit; for explicit updates
|
||||
// see raceTimesChanged(TimePoint, TimePoint, TimePoint).
|
||||
|
||||
if (newStartOfRace != null && newStartOfRace.after(MillisecondsTimePoint.now())) {
|
||||
new Thread(()->
|
||||
// Notify interested users if the new start time is in the future
|
||||
notificationService.notifyUserOnBoatClassUpcomingRace(trackedRace.getRace().getBoatClass(),
|
||||
getMostAppropriateLeaderboard(), getMostAppropriateRaceColumn(), getMostAppropriateFleet(), newStartOfRace),
|
||||
"Notifier about startOfRaceChanged for "+trackedRace).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishedTimeChanged(TimePoint oldFinishedTime, TimePoint newFinishedTime) {
|
||||
// no action required; the update signaled by this call is implicit; the race log
|
||||
// updates that led to this change are replicated separately
|
||||
|
||||
new Thread(()->
|
||||
// Notify interested users:
|
||||
notificationService.notifyUserOnBoatClassRaceChangesStateToFinished(trackedRace.getRace().getBoatClass(), trackedRace,
|
||||
getMostAppropriateLeaderboard(), getMostAppropriateRaceColumn(), getMostAppropriateFleet()),
|
||||
"Notifier about finishedTimeChanged for "+trackedRace).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1713,6 +1843,14 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
public void markPassingReceived(Competitor competitor, Map<Waypoint, MarkPassing> oldMarkPassings,
|
||||
Iterable<MarkPassing> markPassings) {
|
||||
replicate(new UpdateMarkPassings(getRaceIdentifier(), competitor, markPassings));
|
||||
final MarkPassing last = Util.last(markPassings);
|
||||
if (last != null && last.getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint() &&
|
||||
trackedRace.getStatus().getStatus() != TrackedRaceStatusEnum.LOADING) {
|
||||
new Thread(()->
|
||||
// Notify interested users:
|
||||
notificationService.notifyUserOnCompetitorPassesFinish(competitor, trackedRace, getMostAppropriateLeaderboard(), getMostAppropriateRaceColumn(), getMostAppropriateFleet()),
|
||||
"Notifier about finish waypoing passed for "+trackedRace).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1748,6 +1886,62 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
public void raceLogDetached(RaceLog raceLog) {
|
||||
// no action required
|
||||
}
|
||||
|
||||
private Leaderboard getMostAppropriateLeaderboard() {
|
||||
final Triple<Leaderboard, RaceColumn, Fleet> slot = findMostAppropriateLeaderboardSlot();
|
||||
return slot == null ? null : slot.getA();
|
||||
}
|
||||
|
||||
private RaceColumn getMostAppropriateRaceColumn() {
|
||||
final Triple<Leaderboard, RaceColumn, Fleet> slot = findMostAppropriateLeaderboardSlot();
|
||||
return slot == null ? null : slot.getB();
|
||||
}
|
||||
|
||||
private Fleet getMostAppropriateFleet() {
|
||||
final Triple<Leaderboard, RaceColumn, Fleet> slot = findMostAppropriateLeaderboardSlot();
|
||||
return slot == null ? null : slot.getC();
|
||||
}
|
||||
|
||||
/**
|
||||
* When all we have is a {@link #trackedRace} and we're looking for a slot in a leaderboard, we have to
|
||||
* search the enclosing {@link RacingEventService} for a leaderboard that has the {@link #trackedRace}
|
||||
* in a {@link RaceColumn}/{@link Fleet} slot.<p>
|
||||
*
|
||||
* As a first approximation we'll use the fact that a {@link RegattaLeaderboard}'s name is derived
|
||||
* from the {@link Regatta} and as such can be looked up in constant time. Only if such a regatta
|
||||
* leaderboard is not found, a search across all leaderboards will need to be carried out.<p>
|
||||
*
|
||||
* Note that this method should be called in a background thread that runs outside of the call stack
|
||||
* of the notification sent to this {@link RaceChangeListener}, ideally as a task in an executor.
|
||||
* This will avoid performance hits due to an attempt to send out notifications.
|
||||
*/
|
||||
private Triple<Leaderboard, RaceColumn, Fleet> findMostAppropriateLeaderboardSlot() {
|
||||
final Regatta regatta = trackedRace.getTrackedRegatta().getRegatta();
|
||||
final String regattaLeaderboardName = RegattaLeaderboardImpl.getLeaderboardNameForRegatta(regatta);
|
||||
final Leaderboard regattaLeaderboard = getLeaderboardByName(regattaLeaderboardName);
|
||||
Leaderboard leaderboard = null;
|
||||
Pair<RaceColumn, Fleet> raceColumnAndFleet = null;
|
||||
if (regattaLeaderboard != null) {
|
||||
leaderboard = regattaLeaderboard;
|
||||
raceColumnAndFleet = regattaLeaderboard.getRaceColumnAndFleet(trackedRace);
|
||||
} else {
|
||||
for (final Leaderboard l : getLeaderboards().values()) {
|
||||
final Pair<RaceColumn, Fleet> rcaf = l.getRaceColumnAndFleet(trackedRace);
|
||||
if (rcaf != null) {
|
||||
leaderboard = l;
|
||||
raceColumnAndFleet = rcaf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final Triple<Leaderboard, RaceColumn, Fleet> result;
|
||||
if (leaderboard != null && raceColumnAndFleet != null) {
|
||||
result = new Triple<>(leaderboard, raceColumnAndFleet.getA(), raceColumnAndFleet.getB());
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2658,6 +2852,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes
|
||||
LockUtil.lockForWrite(leaderboardsByNameLock);
|
||||
try {
|
||||
leaderboardsByName.clear();
|
||||
scoreCorrectionListenersByLeaderboard.clear();
|
||||
} finally {
|
||||
LockUtil.unlockAfterWrite(leaderboardsByNameLock);
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import com.sap.sailing.server.operationaltransformation.RecordRegattaLogEventOnR
|
||||
* passed to this object's constructor and send a {@link RecordRegattaLogEventOnRegatta} or a
|
||||
* {@link RecordRegattaLogEventOnFlexibleLeaderboard} operation to all replicas.
|
||||
*
|
||||
* This class is a modified version of {@link RaceLogReplicator}
|
||||
* This class is a modified version of {@link RaceLogReplicatorAndNotifier}
|
||||
*
|
||||
*/
|
||||
public class RegattaLogReplicator implements RegattaLikeListener {
|
||||
|
||||
+2
-3
@@ -12,10 +12,9 @@ import com.sap.sse.common.TimePoint;
|
||||
public interface SailingNotificationService {
|
||||
|
||||
/**
|
||||
* Called when {@link RaceState} changes state to finishing/finished (?).
|
||||
* Called when {@link RaceState} changes state to finished.
|
||||
*/
|
||||
void notifyUserOnBoatClassRaceChangesStateToFinishing(BoatClass boatClass, TrackedRace trackedRace, Leaderboard leaderboard, RaceColumn raceColumn,
|
||||
Fleet fleet);
|
||||
void notifyUserOnBoatClassRaceChangesStateToFinished(BoatClass boatClass, TrackedRace trackedRace, Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet);
|
||||
|
||||
/**
|
||||
* Called on score correction for a race with BoatClass if the last score correction was more than one minute.
|
||||
|
||||
+22
-23
@@ -7,7 +7,6 @@ import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.osgi.framework.BundleContext;
|
||||
@@ -49,8 +48,9 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
private final BoatClassResultsNotificationSet boatClassResults;
|
||||
private final BoatClassUpcomingRaceNotificationSet boatClassUpcomingRace;
|
||||
private final CompetitorResultsNotificationSet competitorResults;
|
||||
private final URL defaultBaseURL;
|
||||
|
||||
public SailingNotificationServiceImpl(BundleContext bundleContext, MailQueue mailQueue) {
|
||||
public SailingNotificationServiceImpl(BundleContext bundleContext, MailQueue mailQueue) throws MalformedURLException {
|
||||
this(mailQueue,
|
||||
new BoatClassResultsNotificationSet(bundleContext),
|
||||
new BoatClassUpcomingRaceNotificationSet(bundleContext),
|
||||
@@ -59,8 +59,9 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
|
||||
/**
|
||||
* Constructor used for unit tests to not need BundelContext but directly work with a given UserStore.
|
||||
* @throws MalformedURLException
|
||||
*/
|
||||
public SailingNotificationServiceImpl(UserStore userStore, MailQueue mailQueue) {
|
||||
public SailingNotificationServiceImpl(UserStore userStore, MailQueue mailQueue) throws MalformedURLException {
|
||||
this(mailQueue,
|
||||
new BoatClassResultsNotificationSet(userStore),
|
||||
new BoatClassUpcomingRaceNotificationSet(userStore),
|
||||
@@ -69,10 +70,11 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
|
||||
public SailingNotificationServiceImpl(MailQueue mailQueue, BoatClassResultsNotificationSet boatClassResults,
|
||||
BoatClassUpcomingRaceNotificationSet boatClassUpcomingRace,
|
||||
CompetitorResultsNotificationSet competitorResults) {
|
||||
CompetitorResultsNotificationSet competitorResults) throws MalformedURLException {
|
||||
this.mailQueue = mailQueue;
|
||||
this.messages = new ResourceBundleStringMessagesImpl(STRING_MESSAGES_BASE_NAME,
|
||||
this.getClass().getClassLoader());
|
||||
this.defaultBaseURL = new URL("https://www.sapsailing.com");
|
||||
|
||||
toStop.add(this.boatClassResults = boatClassResults);
|
||||
toStop.add(this.boatClassUpcomingRace = boatClassUpcomingRace);
|
||||
@@ -109,21 +111,22 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
// if multiple LeaderboardGroups of the single event reference the same leaderboard, we just use the
|
||||
// first LeaderboardGroup. This could be a non optimal match but helps to e.g. construct valid links.
|
||||
return new Pair<>(Util.get(foundEvents, 0), Util.get(foundLeaderboardGroups, 0));
|
||||
} else if (foundEventsCount > 1 && foundLeaderboardGroups.size() == 1) {
|
||||
} else if (foundEventsCount > 1) {
|
||||
// could be a series
|
||||
LeaderboardGroup leaderboardGroup = Util.get(foundLeaderboardGroups, 0);
|
||||
if (leaderboardGroup.hasOverallLeaderboard()) {
|
||||
CourseArea defaultCourseArea = leaderboard.getDefaultCourseArea();
|
||||
if (defaultCourseArea != null) {
|
||||
for (Event event : foundEvents) {
|
||||
if (Util.contains(event.getVenue().getCourseAreas(), defaultCourseArea)) {
|
||||
return new Pair<>(event, leaderboardGroup);
|
||||
for (final LeaderboardGroup leaderboardGroup : foundLeaderboardGroups) {
|
||||
if (leaderboardGroup.hasOverallLeaderboard()) {
|
||||
CourseArea defaultCourseArea = leaderboard.getDefaultCourseArea();
|
||||
if (defaultCourseArea != null) {
|
||||
for (Event event : foundEvents) {
|
||||
if (Util.contains(event.getVenue().getCourseAreas(), defaultCourseArea)) {
|
||||
return new Pair<>(event, leaderboardGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Event <-> Leaderboard association is not set up correctly. The UI will show the Leaderboard for
|
||||
// multiple Events. So any of the found Events is ok for this case.
|
||||
return new Pair<>(Util.get(foundEvents, 0), leaderboardGroup);
|
||||
return new Pair<>(Util.get(foundEvents, 0), Util.get(foundLeaderboardGroups, 0));
|
||||
}
|
||||
}
|
||||
// No associated event found or association is ambiguous
|
||||
@@ -190,14 +193,9 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
* to {@code https://www.sapsailing.com} if no base URL has been provided for the event.
|
||||
*/
|
||||
private URL getBaseURL(final Event event) {
|
||||
URL result;
|
||||
final URL result;
|
||||
if (event.getBaseURL() == null) {
|
||||
try {
|
||||
result = new URL("https://www.sapsailing.com");
|
||||
} catch (MalformedURLException e) {
|
||||
logger.log(Level.WARNING, "Strange... why isn't https://www.sapsailing.com a valid URL?", e);
|
||||
result = null;
|
||||
}
|
||||
result = defaultBaseURL;
|
||||
} else {
|
||||
result = event.getBaseURL();
|
||||
}
|
||||
@@ -215,7 +213,7 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyUserOnBoatClassRaceChangesStateToFinishing(BoatClass boatClass, TrackedRace trackedRace,
|
||||
public void notifyUserOnBoatClassRaceChangesStateToFinished(BoatClass boatClass, TrackedRace trackedRace,
|
||||
Leaderboard leaderboard, RaceColumn raceColumn, Fleet fleet) {
|
||||
doWithEvent(leaderboard, (event, leaderboardGroup) -> {
|
||||
String link = createRaceBoardLink(trackedRace, leaderboard, event, leaderboardGroup);
|
||||
@@ -223,13 +221,13 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
mailQueue.addNotification(new NotificationSetNotification<BoatClass>(boatClass, boatClassResults) {
|
||||
@Override
|
||||
protected String constructSubject(BoatClass objectToNotifyAbout, Locale locale) {
|
||||
return messages.get(locale, "boatClassRaceFinishingSubject", boatClass.getDisplayName());
|
||||
return messages.get(locale, "boatClassRaceFinishedSubject", boatClass.getDisplayName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String constructBody(BoatClass objectToNotifyAbout, Locale locale) {
|
||||
String raceDescription = calculateRaceDescription(locale, event, leaderboard, raceColumn, fleet);
|
||||
return messages.get(locale, "boatClassRaceFinishingBody", boatClass.getDisplayName(),
|
||||
return messages.get(locale, "boatClassRaceFinishedBody", boatClass.getDisplayName(),
|
||||
raceDescription, link);
|
||||
}
|
||||
});
|
||||
@@ -238,6 +236,7 @@ public class SailingNotificationServiceImpl implements Stoppable, SailingNotific
|
||||
|
||||
@Override
|
||||
public void notifyUserOnBoatClassWhenScoreCorrectionsAreAvailable(BoatClass boatClass, Leaderboard leaderboard) {
|
||||
// TODO don't send notifications when a notification for the same boatClass / leaderboard has already been sent shortly before
|
||||
doWithEvent(leaderboard, (event, leaderboardGroup) -> {
|
||||
String link = createHomeLeaderboardLink(leaderboard, event);
|
||||
|
||||
|
||||
Reference in new issue
Block a user