mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-08 21:30:57 +00:00
1949 lines
96 KiB
Java
Executable File
1949 lines
96 KiB
Java
Executable File
package com.sap.sailing.server.impl;
|
|
|
|
import java.io.IOException;
|
|
import java.io.ObjectInputStream;
|
|
import java.io.ObjectOutputStream;
|
|
import java.io.Serializable;
|
|
import java.net.MalformedURLException;
|
|
import java.net.SocketException;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.net.URL;
|
|
import java.net.UnknownHostException;
|
|
import java.text.ParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import com.sap.sailing.domain.base.Competitor;
|
|
import com.sap.sailing.domain.base.ControlPoint;
|
|
import com.sap.sailing.domain.base.CourseArea;
|
|
import com.sap.sailing.domain.base.Event;
|
|
import com.sap.sailing.domain.base.Fleet;
|
|
import com.sap.sailing.domain.base.Mark;
|
|
import com.sap.sailing.domain.base.RaceColumn;
|
|
import com.sap.sailing.domain.base.RaceColumnInSeries;
|
|
import com.sap.sailing.domain.base.RaceDefinition;
|
|
import com.sap.sailing.domain.base.Regatta;
|
|
import com.sap.sailing.domain.base.RegattaListener;
|
|
import com.sap.sailing.domain.base.Series;
|
|
import com.sap.sailing.domain.base.Waypoint;
|
|
import com.sap.sailing.domain.base.impl.EventImpl;
|
|
import com.sap.sailing.domain.base.impl.RegattaImpl;
|
|
import com.sap.sailing.domain.common.LeaderboardNameConstants;
|
|
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
|
import com.sap.sailing.domain.common.RegattaIdentifier;
|
|
import com.sap.sailing.domain.common.RegattaName;
|
|
import com.sap.sailing.domain.common.Renamable;
|
|
import com.sap.sailing.domain.common.ScoringSchemeType;
|
|
import com.sap.sailing.domain.common.TimePoint;
|
|
import com.sap.sailing.domain.common.WindSource;
|
|
import com.sap.sailing.domain.common.dto.FleetDTO;
|
|
import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO;
|
|
import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO;
|
|
import com.sap.sailing.domain.common.impl.Util;
|
|
import com.sap.sailing.domain.common.impl.Util.Pair;
|
|
import com.sap.sailing.domain.common.impl.Util.Triple;
|
|
import com.sap.sailing.domain.common.media.MediaTrack;
|
|
import com.sap.sailing.domain.common.media.MediaTrack.MimeType;
|
|
import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard;
|
|
import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn;
|
|
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.ScoringScheme;
|
|
import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.ScoreCorrectionImpl;
|
|
import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl;
|
|
import com.sap.sailing.domain.leaderboard.meta.LeaderboardGroupMetaLeaderboard;
|
|
import com.sap.sailing.domain.persistence.DomainObjectFactory;
|
|
import com.sap.sailing.domain.persistence.MongoFactory;
|
|
import com.sap.sailing.domain.persistence.MongoObjectFactory;
|
|
import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory;
|
|
import com.sap.sailing.domain.persistence.media.DBMediaTrack;
|
|
import com.sap.sailing.domain.persistence.media.MediaDB;
|
|
import com.sap.sailing.domain.persistence.media.MediaDBFactory;
|
|
import com.sap.sailing.domain.racelog.RaceLogStore;
|
|
import com.sap.sailing.domain.swisstimingadapter.Race;
|
|
import com.sap.sailing.domain.swisstimingadapter.SailMasterConnector;
|
|
import com.sap.sailing.domain.swisstimingadapter.SailMasterMessage;
|
|
import com.sap.sailing.domain.swisstimingadapter.SwissTimingFactory;
|
|
import com.sap.sailing.domain.swisstimingadapter.persistence.SwissTimingAdapterPersistence;
|
|
import com.sap.sailing.domain.tracking.DynamicTrackedRegatta;
|
|
import com.sap.sailing.domain.tracking.GPSFix;
|
|
import com.sap.sailing.domain.tracking.GPSFixMoving;
|
|
import com.sap.sailing.domain.tracking.MarkPassing;
|
|
import com.sap.sailing.domain.tracking.RaceChangeListener;
|
|
import com.sap.sailing.domain.tracking.RaceListener;
|
|
import com.sap.sailing.domain.tracking.RaceTracker;
|
|
import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters;
|
|
import com.sap.sailing.domain.tracking.RacesHandle;
|
|
import com.sap.sailing.domain.tracking.TrackedRace;
|
|
import com.sap.sailing.domain.tracking.TrackedRaceStatus;
|
|
import com.sap.sailing.domain.tracking.TrackedRegatta;
|
|
import com.sap.sailing.domain.tracking.Wind;
|
|
import com.sap.sailing.domain.tracking.WindStore;
|
|
import com.sap.sailing.domain.tracking.WindTrack;
|
|
import com.sap.sailing.domain.tracking.WindTracker;
|
|
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
|
|
import com.sap.sailing.domain.tractracadapter.DomainFactory;
|
|
import com.sap.sailing.domain.tractracadapter.JSONService;
|
|
import com.sap.sailing.domain.tractracadapter.RaceRecord;
|
|
import com.sap.sailing.domain.tractracadapter.Receiver;
|
|
import com.sap.sailing.expeditionconnector.ExpeditionListener;
|
|
import com.sap.sailing.expeditionconnector.ExpeditionWindTrackerFactory;
|
|
import com.sap.sailing.expeditionconnector.UDPExpeditionReceiver;
|
|
import com.sap.sailing.mongodb.MongoDBService;
|
|
import com.sap.sailing.operationaltransformation.Operation;
|
|
import com.sap.sailing.server.OperationExecutionListener;
|
|
import com.sap.sailing.server.RacingEventService;
|
|
import com.sap.sailing.server.RacingEventServiceOperation;
|
|
import com.sap.sailing.server.Replicator;
|
|
import com.sap.sailing.server.operationaltransformation.AddCourseArea;
|
|
import com.sap.sailing.server.operationaltransformation.AddDefaultRegatta;
|
|
import com.sap.sailing.server.operationaltransformation.AddMediaTrackOperation;
|
|
import com.sap.sailing.server.operationaltransformation.AddRaceDefinition;
|
|
import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta;
|
|
import com.sap.sailing.server.operationaltransformation.ConnectTrackedRaceToLeaderboardColumn;
|
|
import com.sap.sailing.server.operationaltransformation.CreateEvent;
|
|
import com.sap.sailing.server.operationaltransformation.CreateTrackedRace;
|
|
import com.sap.sailing.server.operationaltransformation.RecordCompetitorGPSFix;
|
|
import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFix;
|
|
import com.sap.sailing.server.operationaltransformation.RecordWindFix;
|
|
import com.sap.sailing.server.operationaltransformation.RemoveEvent;
|
|
import com.sap.sailing.server.operationaltransformation.RemoveMediaTrackOperation;
|
|
import com.sap.sailing.server.operationaltransformation.RemoveWindFix;
|
|
import com.sap.sailing.server.operationaltransformation.RenameEvent;
|
|
import com.sap.sailing.server.operationaltransformation.TrackRegatta;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateEvent;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateMarkPassings;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackDurationOperation;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackStartTimeOperation;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackTitleOperation;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackUrlOperation;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateRaceDelayToLive;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateRaceTimes;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateSpecificRegatta;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateTrackedRaceStatus;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateWindAveragingTime;
|
|
import com.sap.sailing.server.operationaltransformation.UpdateWindSourcesToExclude;
|
|
import com.sap.sailing.util.BuildVersion;
|
|
|
|
public class RacingEventServiceImpl implements RacingEventService, RegattaListener, LeaderboardRegistry, Replicator {
|
|
private static final Logger logger = Logger.getLogger(RacingEventServiceImpl.class.getName());
|
|
|
|
/**
|
|
* A scheduler for the periodic checks of the paramURL documents for the advent of {@link ControlPoint}s
|
|
* with static position information otherwise not available through <code>MarkPassingReceiver</code>'s events.
|
|
*/
|
|
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
|
|
|
private final DomainFactory tractracDomainFactory;
|
|
|
|
private final com.sap.sailing.domain.swisstimingadapter.DomainFactory swissTimingDomainFactory;
|
|
|
|
private final ExpeditionWindTrackerFactory windTrackerFactory;
|
|
|
|
/**
|
|
* Holds the {@link Event} objects for those event registered with this service. Note that there may be {@link Event}
|
|
* objects that exist outside this service for events not (yet) registered here.
|
|
*/
|
|
protected final ConcurrentHashMap<Serializable, Event> eventsById;
|
|
|
|
/**
|
|
* Holds the {@link Regatta} objects for those races registered with this service. Note that there may be {@link Regatta}
|
|
* objects that exist outside this service for regattas not (yet) registered here.
|
|
*/
|
|
protected final ConcurrentHashMap<String, Regatta> regattasByName;
|
|
|
|
private final ConcurrentHashMap<RaceDefinition, CourseChangeReplicator> courseListeners;
|
|
|
|
protected final ConcurrentHashMap<Regatta, Set<RaceTracker>> raceTrackersByRegatta;
|
|
|
|
/**
|
|
* Remembers the trackers by paramURL/liveURI/storedURI to avoid duplication
|
|
*/
|
|
protected final ConcurrentHashMap<Object, RaceTracker> raceTrackersByID;
|
|
|
|
/**
|
|
* Leaderboards managed by this racing event service
|
|
*/
|
|
private final ConcurrentHashMap<String, Leaderboard> leaderboardsByName;
|
|
|
|
private final ConcurrentHashMap<String, LeaderboardGroup> leaderboardGroupsByName;
|
|
|
|
private Set<DynamicTrackedRegatta> regattasObservedForDefaultLeaderboard = new HashSet<DynamicTrackedRegatta>();
|
|
|
|
private final MongoObjectFactory mongoObjectFactory;
|
|
|
|
private final DomainObjectFactory domainObjectFactory;
|
|
|
|
private final SwissTimingFactory swissTimingFactory;
|
|
|
|
private final SwissTimingAdapterPersistence swissTimingAdapterPersistence;
|
|
|
|
private final ConcurrentHashMap<Regatta, DynamicTrackedRegatta> regattaTrackingCache;
|
|
|
|
private final ConcurrentHashMap<OperationExecutionListener, OperationExecutionListener> operationExecutionListeners;
|
|
|
|
/**
|
|
* Keys are the toString() representation of the {@link RaceDefinition#getId() IDs} of races passed to
|
|
* {@link #setRegattaForRace(Regatta, RaceDefinition)}.
|
|
*/
|
|
private final ConcurrentHashMap<String, Regatta> persistentRegattasForRaceIDs;
|
|
|
|
/**
|
|
* The globally used configuration of the time delay (in milliseconds) to the 'live' timepoint used for each new tracked race.
|
|
*/
|
|
private long delayToLiveInMillis;
|
|
|
|
private final RaceLogReplicator raceLogReplicator;
|
|
|
|
private final RaceLogScoringReplicator raceLogScoringReplicator;
|
|
|
|
private final MediaDB mediaDB;
|
|
|
|
private final MediaLibrary mediaLibrary;
|
|
|
|
public RacingEventServiceImpl() {
|
|
this(MongoFactory.INSTANCE.getDefaultDomainObjectFactory(), MongoFactory.INSTANCE.getDefaultMongoObjectFactory(), MediaDBFactory.INSTANCE.getDefaultMediaDB());
|
|
}
|
|
|
|
/**
|
|
* Uses the default factories for the tracking adapters
|
|
*/
|
|
private RacingEventServiceImpl(DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory, MediaDB mediaDB) {
|
|
this(domainObjectFactory, mongoObjectFactory, SwissTimingFactory.INSTANCE,
|
|
com.sap.sailing.domain.swisstimingadapter.DomainFactory.INSTANCE, DomainFactory.INSTANCE, mediaDB);
|
|
}
|
|
|
|
private RacingEventServiceImpl(DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory,
|
|
SwissTimingFactory swissTimingFactory,
|
|
com.sap.sailing.domain.swisstimingadapter.DomainFactory swissTimingDomainFactory,
|
|
DomainFactory tractracDomainFactory, MediaDB mediaDb) {
|
|
assert swissTimingDomainFactory.getBaseDomainFactory() == tractracDomainFactory.getBaseDomainFactory();
|
|
logger.info("Created " + this);
|
|
this.tractracDomainFactory = tractracDomainFactory;
|
|
this.domainObjectFactory = domainObjectFactory;
|
|
this.mongoObjectFactory = mongoObjectFactory;
|
|
this.swissTimingFactory = swissTimingFactory;
|
|
this.swissTimingDomainFactory = swissTimingDomainFactory;
|
|
swissTimingAdapterPersistence = SwissTimingAdapterPersistence.INSTANCE;
|
|
windTrackerFactory = ExpeditionWindTrackerFactory.getInstance();
|
|
regattasByName = new ConcurrentHashMap<String, Regatta>();
|
|
eventsById = new ConcurrentHashMap<Serializable, Event>();
|
|
regattaTrackingCache = new ConcurrentHashMap<Regatta, DynamicTrackedRegatta>();
|
|
raceTrackersByRegatta = new ConcurrentHashMap<Regatta, Set<RaceTracker>>();
|
|
raceTrackersByID = new ConcurrentHashMap<Object, RaceTracker>();
|
|
leaderboardGroupsByName = new ConcurrentHashMap<String, LeaderboardGroup>();
|
|
leaderboardsByName = new ConcurrentHashMap<String, Leaderboard>();
|
|
operationExecutionListeners = new ConcurrentHashMap<OperationExecutionListener, OperationExecutionListener>();
|
|
courseListeners = new ConcurrentHashMap<RaceDefinition, CourseChangeReplicator>();
|
|
persistentRegattasForRaceIDs = new ConcurrentHashMap<String, Regatta>();
|
|
delayToLiveInMillis = TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS;
|
|
this.raceLogReplicator = new RaceLogReplicator(this);
|
|
this.raceLogScoringReplicator = new RaceLogScoringReplicator(this);
|
|
this.mediaDB = mediaDb;
|
|
this.mediaLibrary = new MediaLibrary();
|
|
|
|
// Add one default leaderboard that aggregates all races currently tracked by this service.
|
|
// This is more for debugging purposes than for anything else.
|
|
addFlexibleLeaderboard(LeaderboardNameConstants.DEFAULT_LEADERBOARD_NAME, null, new int[] { 5, 8 },
|
|
tractracDomainFactory.getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), null);
|
|
loadStoredEvents();
|
|
loadStoredRegattas();
|
|
loadRaceIDToRegattaAssociations();
|
|
loadStoredLeaderboardsAndGroups();
|
|
loadMediaLibary();
|
|
}
|
|
|
|
public RacingEventServiceImpl(MongoDBService mongoDBService) {
|
|
this(MongoFactory.INSTANCE.getDomainObjectFactory(mongoDBService), MongoFactory.INSTANCE.getMongoObjectFactory(mongoDBService), MediaDBFactory.INSTANCE.getMediaDB(mongoDBService));
|
|
}
|
|
|
|
public RacingEventServiceImpl(MongoDBService mongoDBService, SwissTimingFactory swissTimingFactory,
|
|
com.sap.sailing.domain.swisstimingadapter.DomainFactory swissTimingDomainFactory,
|
|
DomainFactory tractracDomainFactory, MediaDB mediaDB) {
|
|
this(MongoFactory.INSTANCE.getDomainObjectFactory(mongoDBService), MongoFactory.INSTANCE.getMongoObjectFactory(mongoDBService),
|
|
swissTimingFactory, swissTimingDomainFactory, tractracDomainFactory, mediaDB);
|
|
}
|
|
|
|
@Override
|
|
public com.sap.sailing.domain.base.DomainFactory getBaseDomainFactory() {
|
|
return getTracTracDomainFactory().getBaseDomainFactory();
|
|
}
|
|
|
|
@Override
|
|
public com.sap.sailing.domain.swisstimingadapter.DomainFactory getSwissTimingDomainFactory() {
|
|
return swissTimingDomainFactory;
|
|
}
|
|
|
|
private void loadRaceIDToRegattaAssociations() {
|
|
persistentRegattasForRaceIDs.putAll(domainObjectFactory.loadRaceIDToRegattaAssociations(this));
|
|
}
|
|
|
|
private void loadStoredRegattas() {
|
|
for (Regatta regatta : domainObjectFactory.loadAllRegattas(this)) {
|
|
logger.info("putting regatta "+regatta.getName()+" ("+regatta.hashCode()+") into regattasByName");
|
|
regattasByName.put(regatta.getName(), regatta);
|
|
regatta.addRegattaListener(this);
|
|
regatta.addRaceColumnListener(raceLogReplicator);
|
|
regatta.addRaceColumnListener(raceLogScoringReplicator);
|
|
}
|
|
}
|
|
|
|
private void loadStoredEvents() {
|
|
for (Event event : domainObjectFactory.loadAllEvents()) {
|
|
synchronized (eventsById) {
|
|
if(event.getId() != null)
|
|
eventsById.put(event.getId(), event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects media track references from the configured sources (mongo DB by default, ftp folder yet to be implemented).
|
|
* The method is expected to be called initially blocking the API until finished.
|
|
*
|
|
* Subsequent calls (assumed to be triggered from the admin console or in scheduled intervals) don't need to block. In that case,
|
|
* the API will simply serve the current state.
|
|
*
|
|
*/
|
|
private void loadMediaLibary() {
|
|
Collection<DBMediaTrack> allDBMediaTracks = mediaDB.loadAllMediaTracks();
|
|
for (DBMediaTrack dbMediaTrack : allDBMediaTracks) {
|
|
MimeType mimeType = dbMediaTrack.mimeType != null ? MimeType.valueOf(dbMediaTrack.mimeType) : null;
|
|
MediaTrack mediaTrack = new MediaTrack(dbMediaTrack.dbId, dbMediaTrack.title, dbMediaTrack.url, dbMediaTrack.startTime, dbMediaTrack.durationInMillis, mimeType);
|
|
mediaTrackAdded(mediaTrack);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addLeaderboard(Leaderboard leaderboard) {
|
|
synchronized (leaderboardsByName) {
|
|
leaderboardsByName.put(leaderboard.getName(), leaderboard);
|
|
|
|
// RaceColumns of RegattaLeaderboards are tracked via its Regatta!
|
|
if (leaderboard instanceof FlexibleLeaderboard) {
|
|
leaderboard.addRaceColumnListener(raceLogReplicator);
|
|
leaderboard.addRaceColumnListener(raceLogScoringReplicator);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void loadStoredLeaderboardsAndGroups() {
|
|
logger.info("loading stored leaderboards and groups");
|
|
// Loading all leaderboard groups and the contained leaderboards
|
|
for (LeaderboardGroup leaderboardGroup : domainObjectFactory.getAllLeaderboardGroups(this, this)) {
|
|
logger.info("loaded leaderboard group "+leaderboardGroup.getName()+" into "+this);
|
|
leaderboardGroupsByName.put(leaderboardGroup.getName(), leaderboardGroup);
|
|
}
|
|
// Loading the remaining leaderboards
|
|
domainObjectFactory.getLeaderboardsNotInGroup(this, this);
|
|
logger.info("done with loading stored leaderboards and groups");
|
|
}
|
|
|
|
@Override
|
|
public FlexibleLeaderboard addFlexibleLeaderboard(String leaderboardName, String leaderboardDisplayName, int[] discardThresholds, ScoringScheme scoringScheme,
|
|
Serializable courseAreaId) {
|
|
logger.info("adding flexible leaderboard " + leaderboardName);
|
|
RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
|
|
mongoObjectFactory,
|
|
domainObjectFactory);
|
|
CourseArea courseArea = getCourseArea(courseAreaId);
|
|
FlexibleLeaderboard result = new FlexibleLeaderboardImpl(raceLogStore, leaderboardName, new ScoreCorrectionImpl(), new ThresholdBasedResultDiscardingRuleImpl(
|
|
discardThresholds), scoringScheme, courseArea);
|
|
result.setDisplayName(leaderboardDisplayName);
|
|
synchronized (leaderboardsByName) {
|
|
if (getLeaderboardByName(leaderboardName) != null) {
|
|
throw new IllegalArgumentException("Leaderboard with name "+leaderboardName+" already exists");
|
|
}
|
|
addLeaderboard(result);
|
|
}
|
|
mongoObjectFactory.storeLeaderboard(result);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public CourseArea getCourseArea(Serializable courseAreaId) {
|
|
for (Event event : getAllEvents()) {
|
|
for (CourseArea courseArea : event.getVenue().getCourseAreas()) {
|
|
if (courseArea.getId().equals(courseAreaId)) {
|
|
return courseArea;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public RegattaLeaderboard addRegattaLeaderboard(RegattaIdentifier regattaIdentifier, String leaderboardDisplayName, int[] discardThresholds) {
|
|
Regatta regatta = getRegatta(regattaIdentifier);
|
|
logger.info("adding regatta leaderboard for regatta "
|
|
+ (regatta == null ? "null" : (regatta.getName() + " (" + regatta.hashCode() + ")")) + " to " + this);
|
|
RegattaLeaderboard result = null;
|
|
if (regatta != null) {
|
|
result = new RegattaLeaderboardImpl(regatta, new ScoreCorrectionImpl(), new ThresholdBasedResultDiscardingRuleImpl(
|
|
discardThresholds));
|
|
result.setDisplayName(leaderboardDisplayName);
|
|
synchronized (leaderboardsByName) {
|
|
if (getLeaderboardByName(result.getName()) != null) {
|
|
throw new IllegalArgumentException("Leaderboard with name " + result.getName() + " already exists in "+this);
|
|
}
|
|
addLeaderboard(result);
|
|
}
|
|
mongoObjectFactory.storeLeaderboard(result);
|
|
} else {
|
|
logger.warning("Cannot find regatta "+regattaIdentifier+". Hence, cannot create regatta leaderboard for it.");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public RaceColumn addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null) {
|
|
if (leaderboard instanceof FlexibleLeaderboard) {
|
|
// uses the default fleet as the single fleet for the new column
|
|
RaceColumn result = ((FlexibleLeaderboard) leaderboard).addRaceColumn(columnName, medalRace,
|
|
leaderboard.getFleet(null));
|
|
updateStoredLeaderboard((FlexibleLeaderboard) leaderboard);
|
|
return result;
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " is not a FlexibleLeaderboard");
|
|
}
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void moveLeaderboardColumnUp(String leaderboardName, String columnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) {
|
|
((FlexibleLeaderboard) leaderboard).moveRaceColumnUp(columnName);
|
|
updateStoredLeaderboard((FlexibleLeaderboard) leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void moveLeaderboardColumnDown(String leaderboardName, String columnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) {
|
|
((FlexibleLeaderboard) leaderboard).moveRaceColumnDown(columnName);
|
|
updateStoredLeaderboard((FlexibleLeaderboard) leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeLeaderboardColumn(String leaderboardName, String columnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard == null) {
|
|
throw new IllegalArgumentException("Leaderboard named "+leaderboardName+" not found");
|
|
} else if (!(leaderboard instanceof FlexibleLeaderboard)) {
|
|
throw new IllegalArgumentException("Columns cannot be removed from Leaderboard named "+leaderboardName);
|
|
} else {
|
|
((FlexibleLeaderboard) leaderboard).removeRaceColumn(columnName);
|
|
updateStoredLeaderboard((FlexibleLeaderboard) leaderboard);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null) {
|
|
final RaceColumn raceColumn = leaderboard.getRaceColumnByName(oldColumnName);
|
|
if (raceColumn instanceof FlexibleRaceColumn) {
|
|
((FlexibleRaceColumn) raceColumn).setName(newColumnName);
|
|
updateStoredLeaderboard(leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Race column "+oldColumnName+" cannot be renamed");
|
|
}
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named "+leaderboardName+" not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double factor) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null) {
|
|
final RaceColumn raceColumn = leaderboard.getRaceColumnByName(columnName);
|
|
if (raceColumn != null) {
|
|
raceColumn.setFactor(factor);
|
|
updateStoredLeaderboard(leaderboard);
|
|
} else {
|
|
throw new IllegalArgumentException("Race column "+columnName+" not found in leaderboard "+leaderboardName);
|
|
}
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard named "+leaderboardName+" not found");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void renameLeaderboard(String oldName, String newName) {
|
|
synchronized (leaderboardsByName) {
|
|
if (!leaderboardsByName.containsKey(oldName)) {
|
|
throw new IllegalArgumentException("No leaderboard with name "+oldName+" found");
|
|
}
|
|
if (leaderboardsByName.containsKey(newName)) {
|
|
throw new IllegalArgumentException("Leaderboard with name "+newName+" already exists");
|
|
}
|
|
Leaderboard toRename = leaderboardsByName.get(oldName);
|
|
if (toRename instanceof Renamable) {
|
|
((Renamable) toRename).setName(newName);
|
|
leaderboardsByName.remove(oldName);
|
|
leaderboardsByName.put(newName, toRename);
|
|
mongoObjectFactory.renameLeaderboard(oldName, newName);
|
|
syncGroupsAfterLeaderboardChange(toRename, true);
|
|
} else {
|
|
throw new IllegalArgumentException("Leaderboard with name "+newName+" is of type "+toRename.getClass().getSimpleName()+
|
|
" and therefore cannot be renamed");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void updateStoredLeaderboard(Leaderboard leaderboard) {
|
|
mongoObjectFactory.storeLeaderboard(leaderboard);
|
|
syncGroupsAfterLeaderboardChange(leaderboard, true);
|
|
}
|
|
|
|
@Override
|
|
public void updateStoredRegatta(Regatta regatta) {
|
|
if (regatta.isPersistent()) {
|
|
mongoObjectFactory.storeRegatta(regatta);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks all groups, if they contain a leaderboard with the name of the <code>updatedLeaderboard</code> and
|
|
* replaces the one in the group with the updated one.<br />
|
|
* This synchronizes things like the RaceIdentifier in the leaderboard columns.
|
|
*/
|
|
private void syncGroupsAfterLeaderboardChange(Leaderboard updatedLeaderboard, boolean doDatabaseUpdate) {
|
|
boolean groupNeedsUpdate = false;
|
|
synchronized (leaderboardGroupsByName) {
|
|
for (LeaderboardGroup leaderboardGroup : leaderboardGroupsByName.values()) {
|
|
for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) {
|
|
if (leaderboard == updatedLeaderboard) {
|
|
int index = leaderboardGroup.getIndexOf(leaderboard);
|
|
leaderboardGroup.removeLeaderboard(leaderboard);
|
|
leaderboardGroup.addLeaderboardAt(updatedLeaderboard, index);
|
|
groupNeedsUpdate = true;
|
|
// TODO we assume that the leaderboard names are unique, so we can break the inner loop here
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (doDatabaseUpdate && groupNeedsUpdate) {
|
|
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
|
|
}
|
|
groupNeedsUpdate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeLeaderboard(String leaderboardName) {
|
|
Leaderboard leaderboard = removeLeaderboardFromLeaderboardsByName(leaderboardName);
|
|
leaderboard.removeRaceColumnListener(raceLogReplicator);
|
|
leaderboard.removeRaceColumnListener(raceLogScoringReplicator);
|
|
mongoObjectFactory.removeLeaderboard(leaderboardName);
|
|
syncGroupsAfterLeaderboardRemove(leaderboardName, true);
|
|
leaderboard.destroy();
|
|
}
|
|
|
|
private Leaderboard removeLeaderboardFromLeaderboardsByName(String leaderboardName) {
|
|
synchronized (leaderboardsByName) {
|
|
return leaderboardsByName.remove(leaderboardName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks all groups, if they contain a leaderboard with the <code>removedLeaderboardName</code> and removes it from the group.
|
|
* @param removedLeaderboardName
|
|
*/
|
|
private void syncGroupsAfterLeaderboardRemove(String removedLeaderboardName, boolean doDatabaseUpdate) {
|
|
boolean groupNeedsUpdate = false;
|
|
synchronized (leaderboardGroupsByName) {
|
|
for (LeaderboardGroup leaderboardGroup : leaderboardGroupsByName.values()) {
|
|
for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) {
|
|
if (leaderboard.getName().equals(removedLeaderboardName)) {
|
|
leaderboardGroup.removeLeaderboard(leaderboard);
|
|
groupNeedsUpdate = true;
|
|
// TODO we assume that the leaderboard names are unique, so we can break the inner loop here
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (doDatabaseUpdate && groupNeedsUpdate) {
|
|
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
|
|
}
|
|
groupNeedsUpdate = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Leaderboard getLeaderboardByName(String name) {
|
|
synchronized (leaderboardsByName) {
|
|
return leaderboardsByName.get(name);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Map<String, Leaderboard> getLeaderboards() {
|
|
synchronized (leaderboardsByName) {
|
|
return Collections.unmodifiableMap(new HashMap<String, Leaderboard>(leaderboardsByName));
|
|
}
|
|
}
|
|
|
|
private DomainFactory getTracTracDomainFactory() {
|
|
return tractracDomainFactory;
|
|
}
|
|
|
|
@Override
|
|
public SwissTimingFactory getSwissTimingFactory() {
|
|
return swissTimingFactory;
|
|
}
|
|
|
|
@Override
|
|
public Iterable<Event> getAllEvents() {
|
|
return Collections.unmodifiableCollection(new ArrayList<Event>(eventsById.values()));
|
|
}
|
|
|
|
@Override
|
|
public Event getEvent(Serializable id) {
|
|
return id == null ? null : eventsById.get(id);
|
|
}
|
|
|
|
@Override
|
|
public Iterable<Regatta> getAllRegattas() {
|
|
return Collections.unmodifiableCollection(new ArrayList<Regatta>(regattasByName.values()));
|
|
}
|
|
|
|
@Override
|
|
public boolean isRaceBeingTracked(RaceDefinition r) {
|
|
synchronized (raceTrackersByRegatta) {
|
|
for (Set<RaceTracker> trackers : raceTrackersByRegatta.values()) {
|
|
for (RaceTracker tracker : trackers) {
|
|
if (tracker.getRaces() != null && tracker.getRaces().contains(r)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRegattaByName(String name) {
|
|
return name == null ? null : regattasByName.get(name);
|
|
}
|
|
|
|
@Override
|
|
public Regatta addRegatta(URL jsonURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, WindStore windStore, long timeoutInMilliseconds, String tracTracUsername, String tracTracPassword) throws Exception {
|
|
RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
|
|
mongoObjectFactory,
|
|
domainObjectFactory);
|
|
JSONService jsonService = getTracTracDomainFactory().parseJSONURL(jsonURL);
|
|
Regatta regatta = null;
|
|
for (RaceRecord rr : jsonService.getRaceRecords()) {
|
|
URL paramURL = rr.getParamURL();
|
|
regatta = addTracTracRace(paramURL, liveURI, storedURI, courseDesignUpdateURI, raceLogStore, windStore, timeoutInMilliseconds, tracTracUsername, tracTracPassword).getRegatta();
|
|
}
|
|
return regatta;
|
|
}
|
|
|
|
@Override
|
|
public Regatta getOrCreateDefaultRegatta(String baseRegattaName, String boatClassName, Serializable id) {
|
|
String defaultRegattaName = RegattaImpl.getDefaultName(baseRegattaName, boatClassName);
|
|
|
|
Regatta result = regattasByName.get(defaultRegattaName);
|
|
if (result == null) {
|
|
RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
|
|
mongoObjectFactory,
|
|
domainObjectFactory);
|
|
result = new RegattaImpl(raceLogStore, baseRegattaName, getBaseDomainFactory().getOrCreateBoatClass(
|
|
boatClassName), this, com.sap.sailing.domain.base.DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT),
|
|
id, null);
|
|
logger.info("Created default regatta " + result.getName() + " (" + hashCode() + ") on " + this);
|
|
cacheAndReplicateDefaultRegatta(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public Regatta createRegatta(String baseRegattaName, String boatClassName,
|
|
Serializable id, Iterable<? extends Series> series, boolean persistent, ScoringScheme scoringScheme, Serializable defaultCourseAreaId) {
|
|
RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(
|
|
mongoObjectFactory,
|
|
domainObjectFactory);
|
|
CourseArea courseArea = getCourseArea(defaultCourseAreaId);
|
|
Regatta regatta = new RegattaImpl(raceLogStore, baseRegattaName,
|
|
getBaseDomainFactory().getOrCreateBoatClass(boatClassName), series, persistent, scoringScheme, id, courseArea);
|
|
logger.info("Created regatta " + regatta.getName() + " (" + hashCode() + ") on "+this);
|
|
cacheAndReplicateSpecificRegattaWithoutRaceColumns(regatta);
|
|
if (persistent) {
|
|
updateStoredRegatta(regatta);
|
|
}
|
|
return regatta;
|
|
}
|
|
|
|
@Override
|
|
public Pair<String, List<RaceRecord>> getTracTracRaceRecords(URL jsonURL) throws IOException, ParseException, org.json.simple.parser.ParseException, URISyntaxException {
|
|
JSONService jsonService = getTracTracDomainFactory().parseJSONURL(jsonURL);
|
|
return new Pair<String, List<RaceRecord>>(jsonService.getEventName(), jsonService.getRaceRecords());
|
|
}
|
|
|
|
@Override
|
|
public List<com.sap.sailing.domain.swisstimingadapter.RaceRecord> getSwissTimingRaceRecords(String hostname,
|
|
int port, boolean canSendRequests) throws InterruptedException, UnknownHostException, IOException, ParseException {
|
|
List<com.sap.sailing.domain.swisstimingadapter.RaceRecord> result = new ArrayList<com.sap.sailing.domain.swisstimingadapter.RaceRecord>();
|
|
SailMasterConnector swissTimingConnector = swissTimingFactory.getOrCreateSailMasterConnector(hostname, port, swissTimingAdapterPersistence,
|
|
canSendRequests);
|
|
for (Race race : swissTimingConnector.getRaces()) {
|
|
TimePoint startTime = swissTimingConnector.getStartTime(race.getRaceID());
|
|
result.add(new com.sap.sailing.domain.swisstimingadapter.RaceRecord(race.getRaceID(), race.getDescription(),
|
|
startTime==null?null:startTime.asDate()));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public RacesHandle addSwissTimingRace(RegattaIdentifier regattaToAddTo, String raceID, String hostname,
|
|
int port, boolean canSendRequests, WindStore windStore, RaceLogStore logStore, long timeoutInMilliseconds) throws Exception {
|
|
return addRace(
|
|
regattaToAddTo,
|
|
swissTimingDomainFactory.createTrackingConnectivityParameters(hostname, port, raceID, canSendRequests, delayToLiveInMillis,
|
|
swissTimingFactory, swissTimingDomainFactory, logStore, windStore, swissTimingAdapterPersistence), windStore, timeoutInMilliseconds);
|
|
}
|
|
|
|
@Override
|
|
public RacesHandle addTracTracRace(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, RaceLogStore raceLogStore, WindStore windStore,
|
|
long timeoutInMilliseconds, String tracTracUsername, String tracTracPassword) throws Exception {
|
|
return addRace(
|
|
/* regattaToAddTo */null, getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI,
|
|
/* startOfTracking */null,
|
|
/* endOfTracking */null, delayToLiveInMillis, /* simulateWithStartTimeNow */false, raceLogStore, windStore, tracTracUsername, tracTracPassword), windStore,
|
|
timeoutInMilliseconds);
|
|
}
|
|
|
|
@Override
|
|
public void addRace(RegattaIdentifier addToRegatta, RaceDefinition raceDefinition) {
|
|
Regatta regatta = getRegatta(addToRegatta);
|
|
regatta.addRace(raceDefinition); // will trigger the raceAdded operation because this service is listening on all its regattas
|
|
}
|
|
|
|
/**
|
|
* If the <code>regatta</code> {@link Regatta#isPersistent() is a persistent one}, the association of the race with the
|
|
* regatta is remembered persistently so that {@link #getRememberedRegattaForRace(Serializable)} will provide it.
|
|
*/
|
|
@Override
|
|
public void raceAdded(Regatta regatta, RaceDefinition raceDefinition) {
|
|
if (regatta.isPersistent()) {
|
|
setRegattaForRace(regatta, raceDefinition);
|
|
}
|
|
final CourseChangeReplicator listener = new CourseChangeReplicator(this, regatta, raceDefinition);
|
|
courseListeners.put(raceDefinition, listener);
|
|
raceDefinition.getCourse().addCourseListener(listener);
|
|
replicate(new AddRaceDefinition(regatta.getRegattaIdentifier(), raceDefinition));
|
|
}
|
|
|
|
@Override
|
|
public void raceRemoved(Regatta regatta, RaceDefinition raceDefinition) {
|
|
raceDefinition.getCourse().removeCourseListener(courseListeners.remove(raceDefinition));
|
|
}
|
|
|
|
@Override
|
|
public RacesHandle addRace(RegattaIdentifier regattaToAddTo, RaceTrackingConnectivityParameters params,
|
|
WindStore windStore, long timeoutInMilliseconds) throws Exception {
|
|
RaceTracker tracker = raceTrackersByID.get(params.getTrackerID());
|
|
if (tracker == null) {
|
|
Regatta regatta = regattaToAddTo == null ? null : getRegatta(regattaToAddTo);
|
|
if (regatta == null) {
|
|
// create tracker and use an existing or create a default regatta
|
|
tracker = params.createRaceTracker(this);
|
|
} else {
|
|
// use the regatta selected by the RaceIdentifier regattaToAddTo
|
|
tracker = params.createRaceTracker(regatta, this);
|
|
assert tracker.getRegatta() == regatta;
|
|
}
|
|
synchronized (raceTrackersByRegatta) {
|
|
raceTrackersByID.put(params.getTrackerID(), tracker);
|
|
Set<RaceTracker> trackers = raceTrackersByRegatta.get(tracker.getRegatta());
|
|
if (trackers == null) {
|
|
trackers = new HashSet<RaceTracker>();
|
|
raceTrackersByRegatta.put(tracker.getRegatta(), trackers);
|
|
}
|
|
trackers.add(tracker);
|
|
}
|
|
// TODO we assume here that the event name is unique which necessitates adding the boat class name to it in EventImpl constructor
|
|
String regattaName = tracker.getRegatta().getName();
|
|
Regatta regattaWithName = regattasByName.get(regattaName);
|
|
// TODO we assume here that the event name is unique which necessitates adding the boat class name to it in EventImpl constructor
|
|
if (regattaWithName != null) {
|
|
if (regattaWithName != tracker.getRegatta()) {
|
|
if (Util.isEmpty(regattaWithName.getAllRaces())) {
|
|
// probably, tracker removed the last races from the old regatta and created a new one
|
|
cacheAndReplicateDefaultRegatta(tracker.getRegatta());
|
|
} else {
|
|
throw new RuntimeException("Internal error. Two Event objects with equal name "+regattaName);
|
|
}
|
|
}
|
|
} else {
|
|
cacheAndReplicateDefaultRegatta(tracker.getRegatta());
|
|
}
|
|
} else {
|
|
WindStore existingTrackersWindStore = tracker.getWindStore();
|
|
if (!existingTrackersWindStore.equals(windStore)) {
|
|
logger.warning("Wind store mismatch. Requested wind store: "+windStore+
|
|
". Wind store in use by existing tracker: "+existingTrackersWindStore);
|
|
}
|
|
}
|
|
if (timeoutInMilliseconds != -1) {
|
|
scheduleAbortTrackerAfterInitialTimeout(tracker, timeoutInMilliseconds);
|
|
}
|
|
return tracker.getRacesHandle();
|
|
}
|
|
|
|
/**
|
|
* If <code>regatta</code> is not yet in {@link #regattasByName}, it is added, this service is
|
|
* {@link Regatta#addRegattaListener(RegattaListener) added} as regatta listener, and the regatta and all its
|
|
* contained {@link Regatta#getAllRaces() races} are replicated to all replica.
|
|
*
|
|
* @param regatta
|
|
* the series of this regatta must not have any {@link Series#getRaceColumns() race columns associated
|
|
* (yet)}.
|
|
*/
|
|
private void cacheAndReplicateSpecificRegattaWithoutRaceColumns(Regatta regatta) {
|
|
if (!regattasByName.containsKey(regatta.getName())) {
|
|
logger.info("putting regatta "+regatta.getName()+" ("+regatta.hashCode()+") into regattasByName of "+this);
|
|
regattasByName.put(regatta.getName(), regatta);
|
|
regatta.addRegattaListener(this);
|
|
regatta.addRaceColumnListener(raceLogReplicator);
|
|
regatta.addRaceColumnListener(raceLogScoringReplicator);
|
|
|
|
Serializable courseAreaId = null;
|
|
if (regatta.getDefaultCourseArea() != null) {
|
|
courseAreaId = regatta.getDefaultCourseArea().getId();
|
|
}
|
|
|
|
replicate(new AddSpecificRegatta(
|
|
regatta.getBaseName(), regatta.getBoatClass() == null ? null : regatta
|
|
.getBoatClass().getName(), regatta.getId(), getSeriesWithoutRaceColumnsConstructionParametersAsMap(regatta),
|
|
regatta.isPersistent(), regatta.getScoringScheme(), courseAreaId));
|
|
RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier();
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
replicate(new AddRaceDefinition(regattaIdentifier, race));
|
|
}
|
|
}
|
|
}
|
|
|
|
private RegattaCreationParametersDTO getSeriesWithoutRaceColumnsConstructionParametersAsMap(
|
|
Regatta regatta) {
|
|
LinkedHashMap<String, SeriesCreationParametersDTO> result = new LinkedHashMap<String, SeriesCreationParametersDTO>();
|
|
for (Series s : regatta.getSeries()) {
|
|
assert Util.isEmpty(s.getRaceColumns());
|
|
List<FleetDTO> fleetNamesAndOrdering = new ArrayList<FleetDTO>();
|
|
for (Fleet f : s.getFleets()) {
|
|
fleetNamesAndOrdering.add(getBaseDomainFactory().convertToFleetDTO(f));
|
|
}
|
|
result.put(s.getName(), new SeriesCreationParametersDTO(fleetNamesAndOrdering,
|
|
s.isMedal(), s.isStartsWithZeroScore(), s.isFirstColumnIsNonDiscardableCarryForward(),
|
|
s.getResultDiscardingRule() == null ? null : s.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces()));
|
|
}
|
|
return new RegattaCreationParametersDTO(result);
|
|
}
|
|
|
|
/**
|
|
* If <code>regatta</code> is not yet in {@link #regattasByName}, it is added, this service is
|
|
* {@link Regatta#addRegattaListener(RegattaListener) added} as regatta listener, and the regatta and all its contained
|
|
* {@link Regatta#getAllRaces() races} are replicated to all replica.
|
|
*/
|
|
private void cacheAndReplicateDefaultRegatta(Regatta regatta) {
|
|
if (!regattasByName.containsKey(regatta.getName())) {
|
|
logger.info("putting regatta "+regatta.getName()+" ("+regatta.hashCode()+") into regattasByName of "+this);
|
|
regattasByName.put(regatta.getName(), regatta);
|
|
regatta.addRegattaListener(this);
|
|
regatta.addRaceColumnListener(raceLogReplicator);
|
|
regatta.addRaceColumnListener(raceLogScoringReplicator);
|
|
|
|
replicate(new AddDefaultRegatta(regatta.getBaseName(), regatta.getBoatClass() == null ? null : regatta.getBoatClass().getName(),
|
|
regatta.getId()));
|
|
RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier();
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
replicate(new AddRaceDefinition(regattaIdentifier, race));
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore,
|
|
long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed) {
|
|
DynamicTrackedRegatta trackedRegatta = getOrCreateTrackedRegatta(getRegatta(raceIdentifier));
|
|
RaceDefinition race = getRace(raceIdentifier);
|
|
return trackedRegatta.createTrackedRace(race, windStore, delayToLiveInMillis,
|
|
millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed,
|
|
/* raceDefinitionSetToUpdate */null);
|
|
}
|
|
|
|
@Override
|
|
public RacesHandle addTracTracRace(RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI,
|
|
URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking,
|
|
RaceLogStore raceLogStore, WindStore windStore, long timeoutInMilliseconds, boolean simulateWithStartTimeNow,
|
|
String tracTracUsername, String tracTracPassword) throws Exception {
|
|
return addRace(regattaToAddTo, getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking,
|
|
endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, raceLogStore, windStore, tracTracUsername, tracTracPassword), windStore, timeoutInMilliseconds);
|
|
}
|
|
|
|
private void ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(DynamicTrackedRegatta trackedRegatta) {
|
|
synchronized (regattasObservedForDefaultLeaderboard) {
|
|
if (!regattasObservedForDefaultLeaderboard.contains(trackedRegatta)) {
|
|
trackedRegatta.addRaceListener(new RaceAdditionListener());
|
|
regattasObservedForDefaultLeaderboard.add(trackedRegatta);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(DynamicTrackedRegatta trackedRegatta) {
|
|
synchronized (regattasObservedForDefaultLeaderboard) {
|
|
regattasObservedForDefaultLeaderboard.remove(trackedRegatta);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A listener class used to ensure that when a tracked race is added to any {@link TrackedRegatta} managed by this
|
|
* 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 tracked race is removed again.
|
|
*
|
|
* @author Axel Uhl (d043530)
|
|
*
|
|
*/
|
|
private class RaceAdditionListener implements RaceListener, Serializable {
|
|
private static final long serialVersionUID = 1036955460477000265L;
|
|
|
|
private final Map<TrackedRace, TrackedRaceReplicator> trackedRaceReplicators;
|
|
|
|
public RaceAdditionListener() {
|
|
this.trackedRaceReplicators = new HashMap<TrackedRace, TrackedRaceReplicator>();
|
|
}
|
|
|
|
@Override
|
|
public void raceRemoved(TrackedRace trackedRace) {
|
|
TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace);
|
|
if (trackedRaceReplicator != null) {
|
|
trackedRace.removeListener(trackedRaceReplicator);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void raceAdded(TrackedRace trackedRace) {
|
|
// replicate the addition of the tracked race:
|
|
CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(),
|
|
trackedRace.getDelayToLiveInMillis(),
|
|
trackedRace.getMillisecondsOverWhichToAverageWind(), trackedRace.getMillisecondsOverWhichToAverageSpeed());
|
|
replicate(op);
|
|
linkRaceToConfiguredLeaderboardColumns(trackedRace);
|
|
final FlexibleLeaderboard defaultLeaderboard = (FlexibleLeaderboard) leaderboardsByName.get(LeaderboardNameConstants.DEFAULT_LEADERBOARD_NAME);
|
|
if (defaultLeaderboard != null) {
|
|
String columnName = trackedRace.getRace().getName();
|
|
defaultLeaderboard.addRace(trackedRace, columnName, /* medalRace */false,
|
|
defaultLeaderboard.getFleet(null));
|
|
}
|
|
TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace);
|
|
trackedRaceReplicators.put(trackedRace, trackedRaceReplicator);
|
|
trackedRace.addListener(trackedRaceReplicator); // register as listener first; redundant events for wind fixes are idempotent and don't hurt
|
|
// Now notify all wind fixes we can get from the race by now. TrackedRace.getWindSource() delivers all wind sources known so far.
|
|
// If there is a wind track being loaded, it will be separately notified later by DynamicTrackedRaceImpl.createWindTrack(...).
|
|
for (WindSource windSource : trackedRace.getWindSources()) {
|
|
if (windSource.getType().canBeStored()) {
|
|
WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource);
|
|
// replicate all wind fixed that may have been loaded by the wind store
|
|
windTrack.lockForRead();
|
|
try {
|
|
for (Wind wind : windTrack.getRawFixes()) {
|
|
trackedRaceReplicator.windDataReceived(wind, windSource);
|
|
}
|
|
} finally {
|
|
windTrack.unlockAfterRead();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private class TrackedRaceReplicator implements RaceChangeListener {
|
|
private final TrackedRace trackedRace;
|
|
|
|
public TrackedRaceReplicator(TrackedRace trackedRace) {
|
|
this.trackedRace = trackedRace;
|
|
}
|
|
|
|
@Override
|
|
public void windSourcesToExcludeChanged(Iterable<? extends WindSource> windSourcesToExclude) {
|
|
replicate(new UpdateWindSourcesToExclude(getRaceIdentifier(), windSourcesToExclude));
|
|
}
|
|
|
|
@Override
|
|
public void raceTimesChanged(TimePoint startOfTracking, TimePoint endOfTracking, TimePoint startTimeReceived) {
|
|
replicate(new UpdateRaceTimes(getRaceIdentifier(), startOfTracking, endOfTracking, startTimeReceived));
|
|
}
|
|
|
|
@Override
|
|
public void delayToLiveChanged(long delayToLiveInMillis) {
|
|
replicate(new UpdateRaceDelayToLive(getRaceIdentifier(), delayToLiveInMillis));
|
|
}
|
|
|
|
@Override
|
|
public void windDataReceived(Wind wind, WindSource windSource) {
|
|
replicate(new RecordWindFix(getRaceIdentifier(), windSource, wind));
|
|
}
|
|
|
|
@Override
|
|
public void windDataRemoved(Wind wind, WindSource windSource) {
|
|
replicate(new RemoveWindFix(getRaceIdentifier(), windSource, wind));
|
|
}
|
|
|
|
@Override
|
|
public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
|
|
replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage));
|
|
}
|
|
|
|
@Override
|
|
public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) {
|
|
replicate(new RecordCompetitorGPSFix(getRaceIdentifier(), competitor, fix));
|
|
}
|
|
|
|
@Override
|
|
public void statusChanged(TrackedRaceStatus newStatus) {
|
|
replicate(new UpdateTrackedRaceStatus(getRaceIdentifier(), newStatus));
|
|
}
|
|
|
|
@Override
|
|
public void markPositionChanged(GPSFix fix, Mark mark) {
|
|
replicate(new RecordMarkGPSFix(getRaceIdentifier(), mark, fix));
|
|
}
|
|
|
|
@Override
|
|
public void markPassingReceived(Competitor competitor, Map<Waypoint, MarkPassing> oldMarkPassings, Iterable<MarkPassing> markPassings) {
|
|
replicate(new UpdateMarkPassings(getRaceIdentifier(), competitor, markPassings));
|
|
}
|
|
|
|
@Override
|
|
public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) {
|
|
replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage));
|
|
}
|
|
|
|
private RegattaAndRaceIdentifier getRaceIdentifier() {
|
|
return trackedRace.getRaceIdentifier();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Based on the <code>trackedRace</code>'s {@link TrackedRace#getRaceIdentifier() race identifier}, the tracked race
|
|
* is (re-)associated to all {@link RaceColumn race columns} that currently have no
|
|
* {@link RaceColumn#getTrackedRace(Fleet) tracked race assigned} and whose
|
|
* {@link RaceColumn#getRaceIdentifier(Fleet) race identifier} equals that of <code>trackedRace</code>.
|
|
*/
|
|
private void linkRaceToConfiguredLeaderboardColumns(TrackedRace trackedRace) {
|
|
boolean leaderboardHasChanged = false;
|
|
RegattaAndRaceIdentifier trackedRaceIdentifier = trackedRace.getRaceIdentifier();
|
|
for (Leaderboard leaderboard : getLeaderboards().values()) {
|
|
for (RaceColumn column : leaderboard.getRaceColumns()) {
|
|
for (Fleet fleet : column.getFleets()) {
|
|
if (trackedRaceIdentifier.equals(column.getRaceIdentifier(fleet)) && column.getTrackedRace(fleet) == null) {
|
|
column.setTrackedRace(fleet, trackedRace);
|
|
leaderboardHasChanged = true;
|
|
replicate(new ConnectTrackedRaceToLeaderboardColumn(leaderboard.getName(), column.getName(),
|
|
fleet.getName(), trackedRaceIdentifier));
|
|
}
|
|
}
|
|
}
|
|
if (leaderboardHasChanged) {
|
|
//Update the corresponding groups, to keep them in sync
|
|
syncGroupsAfterLeaderboardChange(leaderboard, /*doDatabaseUpdate*/ false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void stopTracking(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
|
|
synchronized (raceTrackersByRegatta) {
|
|
if (raceTrackersByRegatta.containsKey(regatta)) {
|
|
for (RaceTracker raceTracker : raceTrackersByRegatta.get(regatta)) {
|
|
final Set<RaceDefinition> races = raceTracker.getRaces();
|
|
if (races != null) {
|
|
for (RaceDefinition race : races) {
|
|
stopTrackingWind(regatta, race);
|
|
}
|
|
}
|
|
raceTracker.stop(); // this also removes the TrackedRace from trackedRegatta
|
|
raceTrackersByID.remove(raceTracker.getID());
|
|
}
|
|
raceTrackersByRegatta.remove(regatta);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void stopTrackingAndRemove(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
|
|
stopTracking(regatta);
|
|
if (regatta != null) {
|
|
if (regatta.getName() != null) {
|
|
logger.info("Removing regatta "+regatta.getName()+" ("+regatta.hashCode()+") from "+this);
|
|
regattasByName.remove(regatta.getName());
|
|
regattaTrackingCache.remove(regatta);
|
|
regatta.removeRegattaListener(this);
|
|
regatta.removeRaceColumnListener(raceLogReplicator);
|
|
regatta.removeRaceColumnListener(raceLogScoringReplicator);
|
|
}
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
stopTrackingWind(regatta, race);
|
|
// remove from default leaderboard
|
|
FlexibleLeaderboard defaultLeaderboard = (FlexibleLeaderboard) getLeaderboardByName(LeaderboardNameConstants.DEFAULT_LEADERBOARD_NAME);
|
|
defaultLeaderboard.removeRaceColumn(race.getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The tracker will initially try to connect to the TracTrac infrastructure to obtain basic race master data. If
|
|
* this fails after some timeout, to avoid garbage and lingering threads, the task scheduled by this method will
|
|
* check after the timeout expires if race master data was successfully received. If so, the tracker continues
|
|
* normally. Otherwise, the tracker is shut down orderly by {@link Receiver#stopPreemptively() stopping} all
|
|
* receivers and {@link DataController#stop(boolean) stopping} the TracTrac controller for this tracker.
|
|
*
|
|
* @return the scheduled task, in case the caller wants to {@link ScheduledFuture#cancel(boolean) cancel} it, e.g.,
|
|
* when the tracker is stopped or has successfully received the race
|
|
*/
|
|
private ScheduledFuture<?> scheduleAbortTrackerAfterInitialTimeout(final RaceTracker tracker, final long timeoutInMilliseconds) {
|
|
ScheduledFuture<?> task = getScheduler().schedule(new Runnable() {
|
|
@Override public void run() {
|
|
if (tracker.getRaces() == null || tracker.getRaces().isEmpty()) {
|
|
try {
|
|
Regatta regatta = tracker.getRegatta();
|
|
logger.log(Level.SEVERE, "RaceDefinition for a race in regatta "+regatta.getName()+" not obtained within "+
|
|
timeoutInMilliseconds+"ms. Aborting tracker for this race.");
|
|
Set<RaceTracker> trackersForRegatta;
|
|
synchronized (raceTrackersByRegatta) {
|
|
trackersForRegatta = raceTrackersByRegatta.get(regatta);
|
|
if (trackersForRegatta != null) {
|
|
trackersForRegatta.remove(tracker);
|
|
}
|
|
tracker.stop();
|
|
raceTrackersByID.remove(tracker.getID());
|
|
}
|
|
if (trackersForRegatta == null || trackersForRegatta.isEmpty()) {
|
|
stopTracking(regatta);
|
|
}
|
|
} catch (Exception e) {
|
|
logger.log(Level.SEVERE, "scheduleAbortTrackerAfterInitialTimeout", e);
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}, /* delay */ timeoutInMilliseconds, /* unit */ TimeUnit.MILLISECONDS);
|
|
return task;
|
|
}
|
|
|
|
@Override
|
|
public void stopTracking(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, InterruptedException {
|
|
logger.info("Stopping tracking for "+race+"...");
|
|
synchronized (raceTrackersByRegatta) {
|
|
if (raceTrackersByRegatta.containsKey(regatta)) {
|
|
Iterator<RaceTracker> trackerIter = raceTrackersByRegatta.get(regatta).iterator();
|
|
while (trackerIter.hasNext()) {
|
|
RaceTracker raceTracker = trackerIter.next();
|
|
if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) {
|
|
logger.info("Found tracker to stop for races " + raceTracker.getRaces());
|
|
raceTracker.stop(); // this also removes the TrackedRace from trackedRegatta
|
|
// do not remove the tracker from raceTrackersByRegatta, because it should still exist there,
|
|
// but with the state "non-tracked"
|
|
trackerIter.remove();
|
|
raceTrackersByID.remove(raceTracker.getID());
|
|
}
|
|
}
|
|
} else {
|
|
logger.warning("Didn't find any trackers for regatta "+regatta);
|
|
}
|
|
stopTrackingWind(regatta, race);
|
|
// if the last tracked race was removed, remove the entire regatta
|
|
if (raceTrackersByRegatta.get(regatta).isEmpty()) {
|
|
stopTracking(regatta);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeRegatta(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
removeRace(regatta, race);
|
|
mongoObjectFactory.removeRegattaForRaceID(race.getName(), regatta);
|
|
persistentRegattasForRaceIDs.remove(race.getId().toString());
|
|
}
|
|
if (regatta.isPersistent()) {
|
|
mongoObjectFactory.removeRegatta(regatta);
|
|
}
|
|
regattasByName.remove(regatta.getName());
|
|
regatta.removeRegattaListener(this);
|
|
regatta.removeRaceColumnListener(raceLogReplicator);
|
|
regatta.removeRaceColumnListener(raceLogScoringReplicator);
|
|
}
|
|
|
|
@Override
|
|
public Regatta updateRegatta(RegattaIdentifier regattaIdentifier, Serializable newDefaultCourseAreaId) {
|
|
// We're not doing any renaming of the regatta itself, therefore we don't have to sync on the maps.
|
|
Regatta regatta = getRegatta(regattaIdentifier);
|
|
synchronized(regatta) {
|
|
CourseArea newCourseArea = getCourseArea(newDefaultCourseAreaId);
|
|
if (newCourseArea != regatta.getDefaultCourseArea()) {
|
|
regatta.setDefaultCourseArea(newCourseArea);
|
|
mongoObjectFactory.storeRegatta(regatta);
|
|
}
|
|
|
|
replicate(new UpdateSpecificRegatta(regattaIdentifier, newDefaultCourseAreaId));
|
|
}
|
|
return regatta;
|
|
}
|
|
|
|
@Override
|
|
public void removeRace(Regatta regatta, RaceDefinition race) throws MalformedURLException,
|
|
IOException, InterruptedException {
|
|
logger.info("Removing the race " + race + "...");
|
|
stopAllTrackersForWhichRaceIsLastReachable(regatta, race);
|
|
stopTrackingWind(regatta, race);
|
|
TrackedRace trackedRace = getExistingTrackedRace(regatta, race);
|
|
if (trackedRace != null) {
|
|
TrackedRegatta trackedRegatta = getTrackedRegatta(regatta);
|
|
if (trackedRegatta != null) {
|
|
trackedRegatta.removeTrackedRace(trackedRace);
|
|
}
|
|
if (Util.isEmpty(trackedRegatta.getTrackedRaces())) {
|
|
removeTrackedRegatta(regatta);
|
|
}
|
|
// remove tracked race from RaceColumns of regatta
|
|
boolean regattaChanged = false;
|
|
for (Series series : regatta.getSeries()) {
|
|
for (RaceColumnInSeries raceColumn : series.getRaceColumns()) {
|
|
for (Fleet fleet : series.getFleets()) {
|
|
if (raceColumn.getTrackedRace(fleet) == trackedRace) {
|
|
raceColumn.releaseTrackedRace(fleet);
|
|
regattaChanged = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (regattaChanged) {
|
|
updateStoredRegatta(regatta);
|
|
}
|
|
for (Leaderboard leaderboard : getLeaderboards().values()) {
|
|
if (leaderboard instanceof FlexibleLeaderboard) { // RegattaLeaderboards have implicitly been updated by the code above
|
|
boolean changed = false;
|
|
for (RaceColumn raceColumn : leaderboard.getRaceColumns()) {
|
|
for (Fleet fleet : raceColumn.getFleets()) {
|
|
if (raceColumn.getTrackedRace(fleet) == trackedRace) {
|
|
raceColumn.releaseTrackedRace(fleet); // but leave the RaceIdentifier on the race column
|
|
changed = true; // untouched, e.g., for later re-load
|
|
}
|
|
}
|
|
}
|
|
if (changed) {
|
|
updateStoredLeaderboard((FlexibleLeaderboard) leaderboard);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// remove the race from the regatta if the regatta is not persistently stored
|
|
regatta.removeRace(race);
|
|
if (!regatta.isPersistent() && Util.isEmpty(regatta.getAllRaces())) {
|
|
logger.info("Removing regatta "+regatta.getName()+" ("+regatta.hashCode()+") from service "+this);
|
|
regattasByName.remove(regatta.getName());
|
|
regatta.removeRegattaListener(this);
|
|
regatta.removeRaceColumnListener(raceLogReplicator);
|
|
regatta.removeRaceColumnListener(raceLogScoringReplicator);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Doesn't stop any wind trackers
|
|
*/
|
|
private void stopAllTrackersForWhichRaceIsLastReachable(Regatta regatta, RaceDefinition race)
|
|
throws MalformedURLException, IOException, InterruptedException {
|
|
synchronized (raceTrackersByRegatta) {
|
|
if (raceTrackersByRegatta.containsKey(regatta)) {
|
|
Iterator<RaceTracker> trackerIter = raceTrackersByRegatta.get(regatta).iterator();
|
|
while (trackerIter.hasNext()) {
|
|
RaceTracker raceTracker = trackerIter.next();
|
|
if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) {
|
|
boolean foundReachableRace = false;
|
|
for (RaceDefinition raceTrackedByTracker : raceTracker.getRaces()) {
|
|
if (raceTrackedByTracker != race && isReachable(regatta, raceTrackedByTracker)) {
|
|
foundReachableRace = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!foundReachableRace) {
|
|
// firstly stop the tracker
|
|
raceTracker.stop();
|
|
// remove it from the raceTrackers by Regatta
|
|
trackerIter.remove();
|
|
raceTrackersByID.remove(raceTracker.getID());
|
|
// if the last tracked race was removed, remove the entire regatta
|
|
if (raceTrackersByRegatta.get(regatta).isEmpty()) {
|
|
stopTracking(regatta);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isReachable(Regatta regatta, RaceDefinition race) {
|
|
return Util.contains(regatta.getAllRaces(), race);
|
|
}
|
|
|
|
@Override
|
|
public void startTrackingWind(Regatta regatta, RaceDefinition race,
|
|
boolean correctByDeclination) throws SocketException {
|
|
windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
|
|
}
|
|
|
|
@Override
|
|
public void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException {
|
|
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
|
|
if (windTracker != null) {
|
|
windTracker.stop();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Iterable<Triple<Regatta, RaceDefinition, String>> getWindTrackedRaces() {
|
|
List<Triple<Regatta, RaceDefinition, String>> result = new ArrayList<Triple<Regatta, RaceDefinition, String>>();
|
|
for (Regatta regatta : getAllRegattas()) {
|
|
for (RaceDefinition race : regatta.getAllRaces()) {
|
|
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
|
|
if (windTracker != null) {
|
|
result.add(new Triple<Regatta, RaceDefinition, String>(regatta, race, windTracker.toString()));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace getTrackedRace(Regatta regatta, RaceDefinition race) {
|
|
return getOrCreateTrackedRegatta(regatta).getTrackedRace(race);
|
|
}
|
|
|
|
private TrackedRace getExistingTrackedRace(Regatta regatta, RaceDefinition race) {
|
|
return getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race);
|
|
}
|
|
|
|
@Override
|
|
public DynamicTrackedRegatta getOrCreateTrackedRegatta(Regatta regatta) {
|
|
cacheAndReplicateDefaultRegatta(regatta);
|
|
synchronized (regattaTrackingCache) {
|
|
DynamicTrackedRegatta result = regattaTrackingCache.get(regatta);
|
|
if (result == null) {
|
|
logger.info("Creating DynamicTrackedRegattaImpl for regatta "+regatta.getName()+
|
|
" with hashCode "+regatta.hashCode());
|
|
result = new DynamicTrackedRegattaImpl(regatta);
|
|
replicate(new TrackRegatta(regatta.getRegattaIdentifier()));
|
|
regattaTrackingCache.put(regatta, result);
|
|
ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(result);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public DynamicTrackedRegatta getTrackedRegatta(com.sap.sailing.domain.base.Regatta regatta) {
|
|
return regattaTrackingCache.get(regatta);
|
|
}
|
|
|
|
@Override
|
|
public void removeTrackedRegatta(Regatta regatta) {
|
|
logger.info("Removing regatta "+regatta.getName()+" from regattaTrackingCache");
|
|
DynamicTrackedRegatta trackedRegatta = regattaTrackingCache.remove(regatta);
|
|
stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(trackedRegatta);
|
|
}
|
|
|
|
@Override
|
|
public void storeSwissTimingDummyRace(String racMessage, String stlMessage, String ccgMessage){
|
|
SailMasterMessage racSMMessage = swissTimingFactory.createMessage(racMessage, null);
|
|
SailMasterMessage stlSMMessage = swissTimingFactory.createMessage(stlMessage, null);
|
|
SailMasterMessage ccgSMMessage = swissTimingFactory.createMessage(ccgMessage, null);
|
|
if (swissTimingAdapterPersistence.getRace(stlSMMessage.getRaceID()) != null) {
|
|
throw new IllegalArgumentException("Race with raceID \"" + stlSMMessage.getRaceID() + "\" already exists.");
|
|
}
|
|
else {
|
|
swissTimingAdapterPersistence.storeSailMasterMessage(racSMMessage);
|
|
swissTimingAdapterPersistence.storeSailMasterMessage(stlSMMessage);
|
|
swissTimingAdapterPersistence.storeSailMasterMessage(ccgSMMessage);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRegatta(RegattaName regattaName) {
|
|
return (Regatta) regattasByName.get(regattaName.getRegattaName());
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRegatta(RegattaIdentifier regattaIdentifier) {
|
|
return (Regatta) regattaIdentifier.getRegatta(this);
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace getTrackedRace(RegattaAndRaceIdentifier raceIdentifier) {
|
|
TrackedRace result = null;
|
|
Regatta regatta = regattasByName.get(raceIdentifier.getRegattaName());
|
|
if (regatta != null) {
|
|
DynamicTrackedRegatta trackedRegatta = regattaTrackingCache.get(regatta);
|
|
if (trackedRegatta != null) {
|
|
RaceDefinition race = getRace(raceIdentifier);
|
|
if (race != null) {
|
|
result = trackedRegatta.getTrackedRace(race);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public TrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) {
|
|
Regatta regatta = getRegattaByName(raceIdentifier.getRegattaName());
|
|
TrackedRace trackedRace = null;
|
|
if (regatta != null) {
|
|
RaceDefinition race = regatta.getRaceByName(raceIdentifier.getRaceName());
|
|
trackedRace = getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race);
|
|
}
|
|
return trackedRace;
|
|
}
|
|
|
|
@Override
|
|
public RaceDefinition getRace(RegattaAndRaceIdentifier regattaNameAndRaceName) {
|
|
RaceDefinition result = null;
|
|
Regatta regatta = getRegatta(regattaNameAndRaceName);
|
|
if (regatta != null) {
|
|
result = regatta.getRaceByName(regattaNameAndRaceName.getRaceName());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public Map<String, LeaderboardGroup> getLeaderboardGroups() {
|
|
synchronized (leaderboardGroupsByName) {
|
|
return Collections.unmodifiableMap(new HashMap<String, LeaderboardGroup>(leaderboardGroupsByName));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public LeaderboardGroup getLeaderboardGroupByName(String groupName) {
|
|
synchronized (leaderboardGroupsByName) {
|
|
return leaderboardGroupsByName.get(groupName);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public LeaderboardGroup addLeaderboardGroup(String groupName, String description, boolean displayGroupsInReverseOrder,
|
|
List<String> leaderboardNames, int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) {
|
|
ArrayList<Leaderboard> leaderboards = new ArrayList<>();
|
|
synchronized (leaderboardsByName) {
|
|
for (String leaderboardName : leaderboardNames) {
|
|
Leaderboard leaderboard = leaderboardsByName.get(leaderboardName);
|
|
if (leaderboard == null) {
|
|
throw new IllegalArgumentException("No leaderboard with name " + leaderboardName + " found");
|
|
} else {
|
|
leaderboards.add(leaderboard);
|
|
}
|
|
}
|
|
}
|
|
LeaderboardGroup result = new LeaderboardGroupImpl(groupName, description, displayGroupsInReverseOrder, leaderboards);
|
|
if (overallLeaderboardScoringSchemeType != null) {
|
|
// create overall leaderboard and its discards settings
|
|
addOverallLeaderboardToLeaderboardGroup(result,
|
|
getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType),
|
|
overallLeaderboardDiscardThresholds);
|
|
}
|
|
synchronized (leaderboardGroupsByName) {
|
|
if (leaderboardGroupsByName.containsKey(groupName)) {
|
|
throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists");
|
|
}
|
|
leaderboardGroupsByName.put(groupName, result);
|
|
}
|
|
mongoObjectFactory.storeLeaderboardGroup(result);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public void removeLeaderboardGroup(String groupName) {
|
|
final LeaderboardGroup leaderboardGroup;
|
|
synchronized (leaderboardGroupsByName) {
|
|
leaderboardGroup = leaderboardGroupsByName.remove(groupName);
|
|
}
|
|
mongoObjectFactory.removeLeaderboardGroup(groupName);
|
|
if (leaderboardGroup != null && leaderboardGroup.getOverallLeaderboard() != null) {
|
|
removeLeaderboard(leaderboardGroup.getOverallLeaderboard().getName());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void renameLeaderboardGroup(String oldName, String newName) {
|
|
synchronized (leaderboardGroupsByName) {
|
|
if (!leaderboardGroupsByName.containsKey(oldName)) {
|
|
throw new IllegalArgumentException("No leaderboard group with name " + oldName + " found");
|
|
}
|
|
if (leaderboardGroupsByName.containsKey(newName)) {
|
|
throw new IllegalArgumentException("Leaderboard group with name " + newName + " already exists");
|
|
}
|
|
LeaderboardGroup toRename = leaderboardGroupsByName.remove(oldName);
|
|
toRename.setName(newName);
|
|
leaderboardGroupsByName.put(newName, toRename);
|
|
mongoObjectFactory.renameLeaderboardGroup(oldName, newName);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void updateLeaderboardGroup(String oldName, String newName, String description,
|
|
List<String> leaderboardNames, int[] overallLeaderboardDiscardThresholds,
|
|
ScoringSchemeType overallLeaderboardScoringSchemeType) {
|
|
if (!oldName.equals(newName)) {
|
|
renameLeaderboardGroup(oldName, newName);
|
|
}
|
|
LeaderboardGroup group = getLeaderboardGroupByName(newName);
|
|
if (!description.equals(group.getDescription())) {
|
|
group.setDescriptiom(description);
|
|
}
|
|
group.clearLeaderboards();
|
|
for (String leaderboardName : leaderboardNames) {
|
|
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
|
|
if (leaderboard != null) {
|
|
group.addLeaderboard(leaderboard);
|
|
}
|
|
}
|
|
Leaderboard overallLeaderboard = group.getOverallLeaderboard();
|
|
if (overallLeaderboard != null) {
|
|
if (overallLeaderboardScoringSchemeType == null) {
|
|
group.setOverallLeaderboard(null);
|
|
removeLeaderboard(overallLeaderboard.getName());
|
|
} else {
|
|
// update existing overall leaderboard's discards settings; scoring scheme cannot be updated in-place
|
|
overallLeaderboard.setCrossLeaderboardResultDiscardingRule(new ThresholdBasedResultDiscardingRuleImpl(overallLeaderboardDiscardThresholds));
|
|
updateStoredLeaderboard(overallLeaderboard);
|
|
}
|
|
} else if (overallLeaderboard == null && overallLeaderboardScoringSchemeType != null) {
|
|
addOverallLeaderboardToLeaderboardGroup(group,
|
|
getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType),
|
|
overallLeaderboardDiscardThresholds);
|
|
}
|
|
updateStoredLeaderboardGroup(group);
|
|
}
|
|
|
|
private void addOverallLeaderboardToLeaderboardGroup(LeaderboardGroup leaderboardGroup,
|
|
ScoringScheme scoringScheme, int[] discardThresholds) {
|
|
Leaderboard overallLeaderboard = new LeaderboardGroupMetaLeaderboard(leaderboardGroup, scoringScheme,
|
|
new ThresholdBasedResultDiscardingRuleImpl(discardThresholds));
|
|
leaderboardGroup.setOverallLeaderboard(overallLeaderboard);
|
|
addLeaderboard(overallLeaderboard);
|
|
updateStoredLeaderboard(overallLeaderboard);
|
|
}
|
|
|
|
@Override
|
|
public void updateStoredLeaderboardGroup(LeaderboardGroup leaderboardGroup) {
|
|
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
|
|
}
|
|
|
|
@Override
|
|
public void addExpeditionListener(ExpeditionListener listener, boolean validMessagesOnly) throws SocketException {
|
|
UDPExpeditionReceiver receiver = windTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
|
|
receiver.addListener(listener, validMessagesOnly);
|
|
}
|
|
|
|
@Override
|
|
public void removeExpeditionListener(ExpeditionListener listener) {
|
|
UDPExpeditionReceiver receiver;
|
|
try {
|
|
receiver = windTrackerFactory.getOrCreateWindReceiverOnDefaultPort();
|
|
receiver.removeListener(listener);
|
|
} catch (SocketException e) {
|
|
logger.info("Failed to remove expedition listener "+listener+
|
|
"; exception while trying to retrieve wind receiver: "+e.getMessage());
|
|
}
|
|
}
|
|
|
|
private ScheduledExecutorService getScheduler() {
|
|
return scheduler;
|
|
}
|
|
|
|
/**
|
|
* Currently, the operation is executed by immediately {@link Operation#internalApplyTo(Object) applying} it to this
|
|
* service object.<p>
|
|
*
|
|
* Future implementations of this method will need to also replicate the effects of the operation to all replica
|
|
* of this service known.
|
|
*/
|
|
@Override
|
|
public <T> T apply(RacingEventServiceOperation<T> operation) {
|
|
try {
|
|
T result = operation.internalApplyTo(this);
|
|
replicate(operation);
|
|
return result;
|
|
} catch (Exception e) {
|
|
logger.log(Level.SEVERE, "apply", e);
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public <T> void replicate(RacingEventServiceOperation<T> operation) {
|
|
for (OperationExecutionListener listener : operationExecutionListeners.keySet()) {
|
|
try {
|
|
listener.executed(operation);
|
|
} catch (Exception e) {
|
|
// don't risk the master's operation only because replication to a listener/replica doesn't work
|
|
logger.severe("Error replicating operation "+operation+" to replication listener "+listener);
|
|
logger.log(Level.SEVERE, "replicate", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addOperationExecutionListener(OperationExecutionListener listener) {
|
|
operationExecutionListeners.put(listener, listener);
|
|
}
|
|
|
|
@Override
|
|
public void removeOperationExecutionListener(OperationExecutionListener listener) {
|
|
operationExecutionListeners.remove(listener);
|
|
}
|
|
|
|
@Override
|
|
public void serializeForInitialReplication(ObjectOutputStream oos) throws IOException {
|
|
StringBuffer logoutput = new StringBuffer();
|
|
|
|
logger.info("Serializing events...");
|
|
oos.writeObject(eventsById);
|
|
logoutput.append("\nSerialized " + eventsById.size() + " events\n");
|
|
for (Event event : eventsById.values()) {
|
|
logoutput.append(String.format("%3s\n", event.toString()));
|
|
}
|
|
|
|
logger.info("Serializing regattas...");
|
|
oos.writeObject(regattasByName);
|
|
logoutput.append("Serialized " + regattasByName.size() + " regattas\n");
|
|
for (Regatta regatta : regattasByName.values()) {
|
|
logoutput.append(String.format("%3s\n", regatta.toString()));
|
|
}
|
|
|
|
logger.info("Serializing regattas observed...");
|
|
oos.writeObject(regattasObservedForDefaultLeaderboard);
|
|
logger.info("Serializing regatta tracking cache...");
|
|
oos.writeObject(regattaTrackingCache);
|
|
logger.info("Serializing leaderboard groups...");
|
|
oos.writeObject(leaderboardGroupsByName);
|
|
logoutput.append("Serialized " + leaderboardGroupsByName.size() + " leaderboard groups\n");
|
|
for (LeaderboardGroup lg : leaderboardGroupsByName.values()) {
|
|
logoutput.append(String.format("%3s\n", lg.toString()));
|
|
}
|
|
|
|
logger.info("Serializing leaderboards...");
|
|
oos.writeObject(leaderboardsByName);
|
|
logoutput.append("Serialized " + leaderboardsByName.size() + " leaderboards\n");
|
|
for (Leaderboard lg : leaderboardsByName.values()) {
|
|
logoutput.append(String.format("%3s\n", lg.toString()));
|
|
}
|
|
|
|
logger.info("Serializing media library...");
|
|
mediaLibrary.serialize(oos);
|
|
logoutput.append("Serialized " + mediaLibrary.allTracks().size() + " media tracks\n");
|
|
for (MediaTrack lg : mediaLibrary.allTracks()) {
|
|
logoutput.append(String.format("%3s\n", lg.toString()));
|
|
}
|
|
|
|
logger.info(logoutput.toString());
|
|
}
|
|
|
|
@SuppressWarnings("unchecked") // the type-parameters in the casts of the de-serialized collection objects can't be checked
|
|
@Override
|
|
public void initiallyFillFrom(ObjectInputStream ois) throws IOException, ClassNotFoundException, InterruptedException {
|
|
|
|
logger.info("Performing initial replication load on " + this);
|
|
ClassLoader oldContextClassloader = Thread.currentThread().getContextClassLoader();
|
|
try {
|
|
// Use this object's class's class loader as the context class loader which will then be used for
|
|
// de-serialization; this will cause all classes to be visible that this bundle
|
|
// (com.sap.sailing.server) can see
|
|
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
|
regattasByName.clear();
|
|
regattasObservedForDefaultLeaderboard.clear();
|
|
|
|
if (raceTrackersByRegatta != null && !raceTrackersByRegatta.isEmpty()) {
|
|
for (DynamicTrackedRegatta regatta: regattaTrackingCache.values()) {
|
|
for (RaceTracker tracker : raceTrackersByRegatta.get(regatta)) {
|
|
tracker.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
regattaTrackingCache.clear();
|
|
leaderboardGroupsByName.clear();
|
|
leaderboardsByName.clear();
|
|
eventsById.clear();
|
|
mediaLibrary.clear();
|
|
|
|
StringBuffer logoutput = new StringBuffer();
|
|
|
|
eventsById.putAll((Map<Serializable, Event>) ois.readObject());
|
|
logoutput.append("\nReceived " + eventsById.size() + " NEW events\n");
|
|
for (Event event : eventsById.values()) {
|
|
logoutput.append(String.format("%3s\n", event.toString()));
|
|
}
|
|
|
|
regattasByName.putAll((Map<String, Regatta>) ois.readObject());
|
|
logoutput.append("Received " + regattasByName.size() + " NEW regattas\n");
|
|
for (Regatta regatta : regattasByName.values()) {
|
|
logoutput.append(String.format("%3s\n", regatta.toString()));
|
|
}
|
|
|
|
// it is important that the leaderboards and tracked regattas are cleared before auto-linking to
|
|
// old leaderboards takes place which then don't match the new ones
|
|
for (DynamicTrackedRegatta trackedRegattaToObserve : (Set<DynamicTrackedRegatta>) ois.readObject()) {
|
|
ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(trackedRegattaToObserve);
|
|
}
|
|
|
|
regattaTrackingCache.putAll((Map<Regatta, DynamicTrackedRegatta>) ois.readObject());
|
|
logoutput.append("Received " + regattaTrackingCache.size() + " NEW regatta tracking cache entries\n");
|
|
|
|
leaderboardGroupsByName.putAll((Map<String, LeaderboardGroup>) ois.readObject());
|
|
logoutput.append("Received " + leaderboardGroupsByName.size() + " NEW leaderboard groups\n");
|
|
for (LeaderboardGroup lg : leaderboardGroupsByName.values()) {
|
|
logoutput.append(String.format("%3s\n", lg.toString()));
|
|
}
|
|
|
|
leaderboardsByName.putAll((Map<String, Leaderboard>) ois.readObject());
|
|
logoutput.append("Received " + leaderboardsByName.size() + " NEW leaderboards\n");
|
|
for (Leaderboard leaderboard : leaderboardsByName.values()) {
|
|
logoutput.append(String.format("%3s\n", leaderboard.toString()));
|
|
}
|
|
|
|
// now fix ScoreCorrectionListener setup for LeaderboardGroupMetaLeaderboard instances:
|
|
for (Leaderboard leaderboard : leaderboardsByName.values()) {
|
|
if (leaderboard instanceof LeaderboardGroupMetaLeaderboard) {
|
|
((LeaderboardGroupMetaLeaderboard) leaderboard).registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards();
|
|
}
|
|
}
|
|
|
|
mediaLibrary.deserialize(ois);
|
|
logoutput.append("Received " + mediaLibrary.allTracks().size() + " NEW media tracks\n");
|
|
for (MediaTrack mediatrack : mediaLibrary.allTracks()) {
|
|
logoutput.append(String.format("%3s\n", mediatrack.toString()));
|
|
}
|
|
|
|
logger.info(logoutput.toString());
|
|
logger.info("Done with initial replication on " + this);
|
|
} finally {
|
|
Thread.currentThread().setContextClassLoader(oldContextClassloader);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long getDelayToLiveInMillis() {
|
|
return delayToLiveInMillis;
|
|
}
|
|
|
|
@Override
|
|
public void setDelayToLiveInMillis(long delayToLiveInMillis) {
|
|
this.delayToLiveInMillis = delayToLiveInMillis;
|
|
}
|
|
|
|
@Override
|
|
public Event addEvent(String eventName, String venue, String publicationUrl, boolean isPublic, Serializable id, List<String> courseAreaNames) {
|
|
Event result = new EventImpl(eventName, venue, publicationUrl, isPublic, id);
|
|
synchronized (eventsById) {
|
|
if (eventsById.containsKey(result.getId())) {
|
|
throw new IllegalArgumentException("Event with ID " + result.getId() + " already exists which is pretty surprising...");
|
|
}
|
|
eventsById.put(result.getId(), result);
|
|
replicate(new CreateEvent(eventName, venue, publicationUrl, isPublic, id, courseAreaNames));
|
|
}
|
|
mongoObjectFactory.storeEvent(result);
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public void updateEvent(Serializable id, String eventName, String venueName, String publicationUrl, boolean isPublic, List<String> regattaNames) {
|
|
synchronized (eventsById) {
|
|
if (!eventsById.containsKey(id)) {
|
|
throw new IllegalArgumentException("Sailing event with ID " + id + " does not exist.");
|
|
}
|
|
Event event = eventsById.get(id);
|
|
event.setName(eventName);
|
|
event.setPublicationUrl(publicationUrl);
|
|
event.setPublic(isPublic);
|
|
event.getVenue().setName(venueName);
|
|
|
|
// TODO need to update regattas if they are once linked to event objects
|
|
mongoObjectFactory.storeEvent(event);
|
|
|
|
replicate(new UpdateEvent(id, eventName, venueName, publicationUrl, isPublic, regattaNames));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void renameEvent(Serializable id, String newName) {
|
|
synchronized (eventsById) {
|
|
if (!eventsById.containsKey(id)) {
|
|
throw new IllegalArgumentException("No sailing event with ID "+id+" found.");
|
|
}
|
|
Event toRename = eventsById.get(id);
|
|
toRename.setName(newName);
|
|
mongoObjectFactory.renameEvent(id, newName);
|
|
|
|
replicate(new RenameEvent(id, newName));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeEvent(Serializable id) {
|
|
removeEventFromEventsById(id);
|
|
mongoObjectFactory.removeEvent(id);
|
|
replicate(new RemoveEvent(id));
|
|
}
|
|
|
|
protected void removeEventFromEventsById(Serializable id) {
|
|
synchronized (eventsById) {
|
|
eventsById.remove(id);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Regatta getRememberedRegattaForRace(Serializable raceID) {
|
|
return persistentRegattasForRaceIDs.get(raceID.toString());
|
|
}
|
|
|
|
/**
|
|
* Persistently remembers the association of the race with its {@link RaceDefinition#getId()} to the
|
|
* <code>regatta</code> with its {@link Regatta#getRegattaIdentifier() identifier} so that the next time
|
|
* {@link #getRememberedRegattaForRace(RaceDefinition)} is called with <code>race</code> as argument,
|
|
* <code>regatta</code> will be returned.
|
|
*/
|
|
private void setRegattaForRace(Regatta regatta, RaceDefinition race) {
|
|
persistentRegattasForRaceIDs.put(race.getId().toString(), regatta);
|
|
mongoObjectFactory.storeRegattaForRaceID(race.getId().toString(), regatta);
|
|
}
|
|
|
|
@Override
|
|
public CourseArea addCourseArea(Serializable eventId, String courseAreaName, Serializable courseAreaId) {
|
|
CourseArea courseArea = getBaseDomainFactory().getOrCreateCourseArea(courseAreaId, courseAreaName);
|
|
synchronized (eventsById) {
|
|
if (!eventsById.containsKey(eventId)) {
|
|
throw new IllegalArgumentException("No sailing event with ID " + eventId + " found.");
|
|
}
|
|
Event event = eventsById.get(eventId);
|
|
event.getVenue().addCourseArea(courseArea);
|
|
replicate(new AddCourseArea(eventId, courseAreaName, courseAreaId));
|
|
mongoObjectFactory.storeEvent(event);
|
|
}
|
|
return courseArea;
|
|
}
|
|
|
|
@Override
|
|
public void mediaTrackAdded(MediaTrack mediaTrack) {
|
|
String mimeType = mediaTrack.mimeType != null ? mediaTrack.mimeType.name() : null;
|
|
if (mediaTrack.dbId == null) {
|
|
mediaTrack.dbId = mediaDB.insertMediaTrack(mediaTrack.title, mediaTrack.url, mediaTrack.startTime, mediaTrack.durationInMillis, mimeType);
|
|
}
|
|
mediaLibrary.addMediaTrack(mediaTrack);
|
|
replicate(new AddMediaTrackOperation(mediaTrack));
|
|
}
|
|
|
|
@Override
|
|
public void mediaTracksAdded(Collection<MediaTrack> mediaTracks) {
|
|
mediaLibrary.addMediaTracks(mediaTracks);
|
|
}
|
|
|
|
@Override
|
|
public void mediaTrackTitleChanged(MediaTrack mediaTrack) {
|
|
mediaDB.updateTitle(mediaTrack.dbId, mediaTrack.title);
|
|
mediaLibrary.titleChanged(mediaTrack);
|
|
replicate(new UpdateMediaTrackTitleOperation(mediaTrack));
|
|
}
|
|
|
|
@Override
|
|
public void mediaTrackUrlChanged(MediaTrack mediaTrack) {
|
|
mediaDB.updateUrl(mediaTrack.dbId, mediaTrack.url);
|
|
mediaLibrary.urlChanged(mediaTrack);
|
|
replicate(new UpdateMediaTrackUrlOperation(mediaTrack));
|
|
}
|
|
|
|
@Override
|
|
public void mediaTrackStartTimeChanged(MediaTrack mediaTrack) {
|
|
mediaDB.updateStartTime(mediaTrack.dbId, mediaTrack.startTime);
|
|
mediaLibrary.startTimeChanged(mediaTrack);
|
|
replicate(new UpdateMediaTrackStartTimeOperation(mediaTrack));
|
|
}
|
|
|
|
@Override
|
|
public void mediaTrackDurationChanged(MediaTrack mediaTrack) {
|
|
mediaDB.updateDuration(mediaTrack.dbId, mediaTrack.durationInMillis);
|
|
mediaLibrary.durationChanged(mediaTrack);
|
|
replicate(new UpdateMediaTrackDurationOperation(mediaTrack));
|
|
}
|
|
|
|
@Override
|
|
public void mediaTrackDeleted(MediaTrack mediaTrack) {
|
|
mediaDB.deleteMediaTrack(mediaTrack.dbId);
|
|
mediaLibrary.deleteMediaTrack(mediaTrack);
|
|
replicate(new RemoveMediaTrackOperation(mediaTrack));
|
|
}
|
|
|
|
@Override
|
|
public Collection<MediaTrack> getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) {
|
|
TrackedRace trackedRace = getExistingTrackedRace(regattaAndRaceIdentifier);
|
|
if (trackedRace != null) {
|
|
Date raceStart = trackedRace.getStartOfRace() == null ? null : trackedRace.getStartOfRace().asDate();
|
|
Date raceEnd = trackedRace.getEndOfRace() == null ? null : trackedRace.getEndOfRace().asDate();
|
|
return mediaLibrary.findMediaTracksInTimeRange(raceStart , raceEnd);
|
|
} else {
|
|
return Collections.emptyList();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Collection<MediaTrack> getAllMediaTracks() {
|
|
return mediaLibrary.allTracks();
|
|
}
|
|
|
|
public String toString() {
|
|
return "RacingEventService: " + this.hashCode() + " Build: " + BuildVersion.getBuildVersion();
|
|
}
|
|
|
|
}
|