From d6419c51cb4813383e903f5afa3e7648bf8118a2 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 15 Jul 2016 18:38:24 +0200 Subject: [PATCH] wired user notifications to a new LeaderboardScoreCorrectionNotifier in RacingEventService; wired notifications with TrackedRaceReplicatorAndNotifier Change-Id: I4ff77cf49700140b134dc95326a3f7a844958d95 --- .../impl/DomainFactoryImpl.java | 8 +- .../domain/leaderboard/Leaderboard.java | 16 ++ .../leaderboard/LeaderboardCacheManager.java | 4 +- .../leaderboard/ScoreCorrectionListener.java | 2 +- .../leaderboard/SettableScoreCorrection.java | 4 + .../impl/AbstractSimpleLeaderboardImpl.java | 23 ++ .../impl/RegattaLeaderboardImpl.java | 14 +- .../leaderboard/impl/ScoreCorrectionImpl.java | 8 +- .../meta/AbstractMetaLeaderboard.java | 6 +- .../meta/MetaLeaderboardScoreCorrection.java | 6 +- .../test/AbstractServerReplicationTest.java | 4 +- .../stringmessages/StringMessages.properties | 4 +- .../sap/sailing/server/impl/Activator.java | 2 +- ...java => RaceLogReplicatorAndNotifier.java} | 15 +- .../server/impl/RacingEventServiceImpl.java | 271 +++++++++++++++--- .../server/impl/RegattaLogReplicator.java | 2 +- .../SailingNotificationService.java | 5 +- .../impl/SailingNotificationServiceImpl.java | 45 ++- 18 files changed, 346 insertions(+), 93 deletions(-) rename java/com.sap.sailing.server/src/com/sap/sailing/server/impl/{RaceLogReplicator.java => RaceLogReplicatorAndNotifier.java} (82%) diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index 2ce3c3f76ae..6d721a5e89b 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -612,18 +612,18 @@ public class DomainFactoryImpl implements DomainFactory { @Override public BoatClass getDominantBoatClass(Iterable competitorClassNames) { + final BoatClass result; if (competitorClassNames == null) { - return null; - + result = null; } else { Collection boatClasses = new ArrayList<>(); for (String competitorClassName : competitorClassNames) { BoatClass boatClass = getOrCreateBoatClass(competitorClassName); boatClasses.add(boatClass); } - - return Util.getDominantObject(boatClasses); + result = Util.getDominantObject(boatClasses); } + return result; } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java index d0635587a1f..f8ba35fa65c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseArea; import com.sap.sailing.domain.base.DomainFactory; @@ -28,6 +29,7 @@ import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; /** * A leaderboard is used to display the results of one or more {@link TrackedRace races}. It manages the competitors' @@ -532,4 +534,18 @@ public interface Leaderboard extends LeaderboardBase, HasRaceColumns { NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher(); + /** + * Looks through all {@link #getRaceColumns() race columns} and their {@link RaceColumn#getFleets() fleets} and checks + * if {@code trackedRace} is {@link RaceColumn#getTrackedRace(Fleet) linked} to that combination. If such a slot is found + * that "slot" is returned by a pair specifying the non-{@code null} {@link RaceColumn} and {@code Fleet} pair. Otherwise, + * {@code null} is returned. + */ + Pair getRaceColumnAndFleet(TrackedRace trackedRace); + + /** + * Gets the ("dominant") boat class for this leaderboard. For a {@link RegattaLeaderboard} this is the {@link Regatta}'s boat class. + * For a {@link FlexibleLeaderboard} the implementation is more complex because no fixed boat class is set for the leaderboard. There, + * the boat class will be determined based on the most frequently occurring boat class when iterating across the competitors. + */ + BoatClass getBoatClass(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java index 2dd3fc8375c..a4ba3c0c0ec 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/LeaderboardCacheManager.java @@ -160,8 +160,8 @@ public class LeaderboardCacheManager { } @Override - public void maxPointsReasonChanced(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { + public void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { removeFromCache(leaderboard); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java index 0cdf41e2f3f..eb64022400d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoreCorrectionListener.java @@ -8,7 +8,7 @@ import com.sap.sse.common.TimePoint; public interface ScoreCorrectionListener { void correctedScoreChanced(Competitor competitor, RaceColumn raceColumn, Double oldCorrectedScore, Double newCorrectedScore); - void maxPointsReasonChanced(Competitor competitor, MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason); + void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason); void carriedPointsChanged(Competitor competitor, Double oldCarriedPoints, Double newCarriedPoints); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java index 7d5cad8055b..10b1eaeb283 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/SettableScoreCorrection.java @@ -6,6 +6,10 @@ import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sse.common.TimePoint; public interface SettableScoreCorrection extends ScoreCorrection { + /** + * The listeners added are transient and do not need to be {@link Serializable}. When this object is + * de-serialized, it starts out with an empty set of listeners. + */ void addScoreCorrectionListener(ScoreCorrectionListener listener); void removeScoreCorrectionListener(ScoreCorrectionListener listener); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index 30894929bd4..e6833b076e3 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -29,10 +29,13 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.DomainFactory; @@ -93,6 +96,7 @@ import com.sap.sailing.util.impl.RaceColumnListeners; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; @@ -1938,6 +1942,25 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race return new NumberOfCompetitorsFetcherImpl(); } + @Override + public Pair getRaceColumnAndFleet(TrackedRace trackedRace) { + for (final RaceColumn raceColumn : getRaceColumns()) { + for (final Fleet fleet : raceColumn.getFleets()) { + if (raceColumn.getTrackedRace(fleet) == trackedRace) { + return new Pair<>(raceColumn, fleet); + } + } + } + return null; + } + + + @Override + public BoatClass getBoatClass() { + return Util.getDominantObject(StreamSupport.stream(getCompetitors().spliterator(), /* parallel */ false). + map(c->c.getBoat().getBoatClass()).collect(Collectors.toList())); + } + protected abstract LeaderboardType getLeaderboardType(); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java index d5af239cf6a..72cfd8ca4d4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CourseArea; import com.sap.sailing.domain.base.Fleet; @@ -53,10 +54,14 @@ public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements R public Regatta getRegatta() { return regatta; } - + + public static String getLeaderboardNameForRegatta(Regatta regatta) { + return regatta.getName(); + } + @Override public String getName() { - return getRegatta().getName(); + return getLeaderboardNameForRegatta(getRegatta()); } @Override @@ -120,4 +125,9 @@ public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements R protected LeaderboardType getLeaderboardType() { return LeaderboardType.RegattaLeaderboard; } + + @Override + public BoatClass getBoatClass() { + return getRegatta().getBoatClass(); + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java index 8b2e2d59ca4..47d1e4fb489 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java @@ -103,10 +103,10 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { } } - protected void notifyListeners(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { + protected void notifyListeners(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { for (ScoreCorrectionListener listener : getScoreCorrectionListeners()) { - listener.maxPointsReasonChanced(competitor, oldMaxPointsReason, newMaxPointsReason); + listener.maxPointsReasonChanced(competitor, raceColumn, oldMaxPointsReason, newMaxPointsReason); } } @@ -148,7 +148,7 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { } else { oldMaxPointsReason = maxPointsReasons.put(key, reason); } - notifyListeners(competitor, oldMaxPointsReason, reason); + notifyListeners(competitor, raceColumn, oldMaxPointsReason, reason); } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java index 030b22f75db..460ab3af697 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/AbstractMetaLeaderboard.java @@ -74,9 +74,9 @@ public abstract class AbstractMetaLeaderboard extends AbstractSimpleLeaderboardI } @Override - public void maxPointsReasonChanced(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { - getScoreCorrection().notifyListeners(competitor, oldMaxPointsReason, newMaxPointsReason); + public void maxPointsReasonChanced(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { + getScoreCorrection().notifyListeners(competitor, raceColumn, oldMaxPointsReason, newMaxPointsReason); } @Override diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java index 11206d8011e..7e5643534eb 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/MetaLeaderboardScoreCorrection.java @@ -36,9 +36,9 @@ public class MetaLeaderboardScoreCorrection extends ScoreCorrectionImpl { * These redefinitions are required for scoping reasons, to make the method visible also to other classes in this package. */ @Override - protected void notifyListeners(Competitor competitor, MaxPointsReason oldMaxPointsReason, - MaxPointsReason newMaxPointsReason) { - super.notifyListeners(competitor, oldMaxPointsReason, newMaxPointsReason); + protected void notifyListeners(Competitor competitor, RaceColumn raceColumn, + MaxPointsReason oldMaxPointsReason, MaxPointsReason newMaxPointsReason) { + super.notifyListeners(competitor, raceColumn, oldMaxPointsReason, newMaxPointsReason); } @Override diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java index 269a75ebe99..8a729da4acf 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java @@ -55,7 +55,7 @@ public abstract class AbstractServerReplicationTest extends com.sap.sse.replicat @Override public DomainFactory getBaseDomainFactory() { return baseDomainFactory; } @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; - }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, null, null); + }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, null, null, /* sailingNotificationService */ null); } @Override @@ -74,7 +74,7 @@ public abstract class AbstractServerReplicationTest extends com.sap.sse.replicat @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } }; }, MediaDBFactory.INSTANCE.getMediaDB(mongoDBService), EmptyWindStore.INSTANCE, EmptySensorFixStore.INSTANCE, - /* serviceFinderFactory */ null, null); + /* serviceFinderFactory */ null, null, /* sailingNotificationService */ null); } } } diff --git a/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties b/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties index aa925481168..42b4ed01af8 100644 --- a/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties +++ b/java/com.sap.sailing.server/resources/stringmessages/StringMessages.properties @@ -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 diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java index 81649269e50..8695253dc8c 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/Activator.java @@ -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( diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicatorAndNotifier.java similarity index 82% rename from java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java rename to java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicatorAndNotifier.java index df2e1f6b302..a231a79a8a5 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogReplicatorAndNotifier.java @@ -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.

+ * + * 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; } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 5775aba10a2..d94b2eff851 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -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 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.

+ * + * 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 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 true, the {@link PersistentCompetitorStore} is created empty, with the corresponding * database collection cleared as well. Use with caution! When used with false, 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 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 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 trackedRaceReplicators; + private final Map trackedRaceReplicators; private final Map polarFixCacheUpdaters; public RaceAdditionListener() { - this.trackedRaceReplicators = new HashMap(); + this.trackedRaceReplicators = new HashMap(); this.polarFixCacheUpdaters = new HashMap(); } @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 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 oldMarkPassings, Iterable 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 slot = findMostAppropriateLeaderboardSlot(); + return slot == null ? null : slot.getA(); + } + + private RaceColumn getMostAppropriateRaceColumn() { + final Triple slot = findMostAppropriateLeaderboardSlot(); + return slot == null ? null : slot.getB(); + } + + private Fleet getMostAppropriateFleet() { + final Triple 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.

+ * + * 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.

+ * + * 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 findMostAppropriateLeaderboardSlot() { + final Regatta regatta = trackedRace.getTrackedRegatta().getRegatta(); + final String regattaLeaderboardName = RegattaLeaderboardImpl.getLeaderboardNameForRegatta(regatta); + final Leaderboard regattaLeaderboard = getLeaderboardByName(regattaLeaderboardName); + Leaderboard leaderboard = null; + Pair raceColumnAndFleet = null; + if (regattaLeaderboard != null) { + leaderboard = regattaLeaderboard; + raceColumnAndFleet = regattaLeaderboard.getRaceColumnAndFleet(trackedRace); + } else { + for (final Leaderboard l : getLeaderboards().values()) { + final Pair rcaf = l.getRaceColumnAndFleet(trackedRace); + if (rcaf != null) { + leaderboard = l; + raceColumnAndFleet = rcaf; + break; + } + } + } + final Triple 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); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java index 928825e5055..f29f2329e91 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RegattaLogReplicator.java @@ -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 { diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java index 2f8061df735..acd48c15a88 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/SailingNotificationService.java @@ -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. diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java index 55314c9eb6b..56a7e41cf6c 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/notification/impl/SailingNotificationServiceImpl.java @@ -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, 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);