Files
sailing-analytics/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java
T
Frederik Petersen c283d9276a Introduces oldStatus parameter for state change listener
polar service can now listen for state changes (loading -> x)
2015-03-25 10:51:47 +01:00

3109 lines
149 KiB
Java
Executable File

package com.sap.sailing.server.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import com.sap.sailing.domain.abstractlog.AbstractLog;
import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor;
import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl;
import com.sap.sailing.domain.abstractlog.race.RaceLog;
import com.sap.sailing.domain.abstractlog.race.RaceLogEvent;
import com.sap.sailing.domain.abstractlog.race.RaceLogEventVisitor;
import com.sap.sailing.domain.abstractlog.race.state.RaceState;
import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState;
import com.sap.sailing.domain.abstractlog.race.state.impl.RaceStateImpl;
import com.sap.sailing.domain.abstractlog.race.state.impl.ReadonlyRaceStateImpl;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CompetitorStore;
import com.sap.sailing.domain.base.CompetitorStore.CompetitorUpdateListener;
import com.sap.sailing.domain.base.ControlPoint;
import com.sap.sailing.domain.base.CourseArea;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Event;
import com.sap.sailing.domain.base.EventBase;
import com.sap.sailing.domain.base.EventFetcher;
import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.LeaderboardSearchResult;
import com.sap.sailing.domain.base.LeaderboardSearchResultBase;
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.RemoteSailingServerReference;
import com.sap.sailing.domain.base.Series;
import com.sap.sailing.domain.base.Sideline;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.configuration.DeviceConfiguration;
import com.sap.sailing.domain.base.configuration.DeviceConfigurationIdentifier;
import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher;
import com.sap.sailing.domain.base.configuration.RegattaConfiguration;
import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMapImpl;
import com.sap.sailing.domain.base.impl.DynamicCompetitor;
import com.sap.sailing.domain.base.impl.EventImpl;
import com.sap.sailing.domain.base.impl.RegattaImpl;
import com.sap.sailing.domain.base.impl.RemoteSailingServerReferenceImpl;
import com.sap.sailing.domain.common.DataImportProgress;
import com.sap.sailing.domain.common.LeaderboardNameConstants;
import com.sap.sailing.domain.common.Position;
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.TrackedRaceStatusEnum;
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.DataImportProgressImpl;
import com.sap.sailing.domain.common.media.MediaTrack;
import com.sap.sailing.domain.common.racelog.RacingProcedureType;
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.ThresholdBasedResultDiscardingRuleImpl;
import com.sap.sailing.domain.leaderboard.meta.LeaderboardGroupMetaLeaderboard;
import com.sap.sailing.domain.persistence.DomainObjectFactory;
import com.sap.sailing.domain.persistence.MongoObjectFactory;
import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory;
import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory;
import com.sap.sailing.domain.persistence.MongoWindStore;
import com.sap.sailing.domain.persistence.MongoWindStoreFactory;
import com.sap.sailing.domain.persistence.PersistenceFactory;
import com.sap.sailing.domain.persistence.media.MediaDB;
import com.sap.sailing.domain.persistence.media.MediaDBFactory;
import com.sap.sailing.domain.persistence.racelog.tracking.MongoGPSFixStoreFactory;
import com.sap.sailing.domain.racelog.RaceLogIdentifier;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.domain.racelog.tracking.GPSFixStore;
import com.sap.sailing.domain.racelogtracking.DeviceIdentifier;
import com.sap.sailing.domain.regattalike.IsRegattaLike;
import com.sap.sailing.domain.regattalike.LeaderboardThatHasRegattaLike;
import com.sap.sailing.domain.regattalog.RegattaLogStore;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
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.GPSFixTrack;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.RaceChangeListener;
import com.sap.sailing.domain.tracking.RaceHandle;
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.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.WindTracker;
import com.sap.sailing.domain.tracking.WindTrackerFactory;
import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener;
import com.sap.sailing.domain.tracking.impl.DynamicGPSFixTrackImpl;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl;
import com.sap.sailing.expeditionconnector.ExpeditionWindTrackerFactory;
import com.sap.sailing.polars.PolarDataService;
import com.sap.sailing.polars.factory.PolarDataServiceFactory;
import com.sap.sailing.server.RacingEventService;
import com.sap.sailing.server.Replicator;
import com.sap.sailing.server.gateway.deserialization.impl.CourseAreaJsonDeserializer;
import com.sap.sailing.server.gateway.deserialization.impl.EventBaseJsonDeserializer;
import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJsonDeserializer;
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.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.CreateOrUpdateDataImportProgress;
import com.sap.sailing.server.operationaltransformation.CreateOrUpdateDeviceConfiguration;
import com.sap.sailing.server.operationaltransformation.CreateTrackedRace;
import com.sap.sailing.server.operationaltransformation.DataImportFailed;
import com.sap.sailing.server.operationaltransformation.RecordCompetitorGPSFix;
import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFix;
import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFixForExistingTrack;
import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFixForNewMarkTrack;
import com.sap.sailing.server.operationaltransformation.RecordWindFix;
import com.sap.sailing.server.operationaltransformation.RemoveDeviceConfiguration;
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.SetDataImportDeleteProgressFromMapTimer;
import com.sap.sailing.server.operationaltransformation.TrackRegatta;
import com.sap.sailing.server.operationaltransformation.UpdateCompetitor;
import com.sap.sailing.server.operationaltransformation.UpdateEndOfTracking;
import com.sap.sailing.server.operationaltransformation.UpdateMarkPassings;
import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackDurationOperation;
import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackRacesOperation;
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.UpdateStartOfTracking;
import com.sap.sailing.server.operationaltransformation.UpdateStartTimeReceived;
import com.sap.sailing.server.operationaltransformation.UpdateTrackedRaceStatus;
import com.sap.sailing.server.operationaltransformation.UpdateWindAveragingTime;
import com.sap.sailing.server.operationaltransformation.UpdateWindSourcesToExclude;
import com.sap.sse.BuildVersion;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.TypeBasedServiceFinderFactory;
import com.sap.sse.common.Util;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.common.search.KeywordQuery;
import com.sap.sse.common.search.Result;
import com.sap.sse.common.search.ResultImpl;
import com.sap.sse.concurrent.LockUtil;
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
import com.sap.sse.filestorage.FileStorageManagementService;
import com.sap.sse.replication.OperationExecutionListener;
import com.sap.sse.replication.OperationWithResult;
import com.sap.sse.replication.ReplicationMasterDescriptor;
import com.sap.sse.replication.impl.OperationWithResultWithIdWrapper;
import com.sap.sse.util.ClearStateTestSupport;
public class RacingEventServiceImpl implements RacingEventService, ClearStateTestSupport, RegattaListener,
LeaderboardRegistry, Replicator, EventFetcher {
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 com.sap.sailing.domain.base.DomainFactory baseDomainFactory;
/**
* 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.
*/
private final ConcurrentHashMap<Serializable, Event> eventsById;
private final RemoteSailingServerSet remoteSailingServerSet;
/**
* 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 NamedReentrantReadWriteLock regattasByNameLock;
private final ConcurrentHashMap<RaceDefinition, CourseChangeReplicator> courseListeners;
protected final ConcurrentHashMap<Regatta, Set<RaceTracker>> raceTrackersByRegatta;
/**
* Although {@link #raceTrackersByRegatta} is a concurrent hash map, entering sets as values needs to be
* synchronized using this lock's write lock to avoid two value sets overwriting each other.
*/
private final NamedReentrantReadWriteLock raceTrackersByRegattaLock;
/**
* Remembers the trackers by paramURL/liveURI/storedURI to avoid duplication
*/
protected final ConcurrentHashMap<Object, RaceTracker> raceTrackersByID;
/**
* {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)} will check
* {@link #raceTrackersByID} for the presence of a tracker and won't create a new tracker if one for the
* connectivity parameters' ID already exists. This check and creation and addition to {@link #raceTrackersByID}
* requires locking in the face of concurrent calls to
* {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)}. Using <code>synchronized</code> is
* not ideal due to its coarse-grained locking style which allows for little concurrency. Instead, this map is used
* to keep locks for any ID that any invocation of the
* {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)} method is currently working on.
* Fetching or creating and putting a lock to this map happens in ({@link #getOrCreateRaceTrackersByIdLock}) which
* takes care of managing concurrent access to this concurrent map. When done, the
* {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)} method cleans up by removing the
* lock again from this map, again using a synchronized method (
* {@link #unlockRaceTrackersById(Object, NamedReentrantReadWriteLock)}).
*/
private final ConcurrentHashMap<Object, NamedReentrantReadWriteLock> raceTrackersByIDLocks;
/**
* Leaderboards managed by this racing event service
*/
private final ConcurrentHashMap<String, Leaderboard> leaderboardsByName;
/**
* {@link #leaderboardsByName} is already a concurrent hash map; however, when renaming a leaderboard, this shall
* happen as an atomic transaction, not interruptible by other write accesses on the same map because otherwise
* assumptions made during the rename process wouldn't hold. See, in particular,
* {@link #renameLeaderboard(String, String)}.
*/
private final NamedReentrantReadWriteLock leaderboardsByNameLock;
private final ConcurrentHashMap<String, LeaderboardGroup> leaderboardGroupsByName;
private final ConcurrentHashMap<UUID, LeaderboardGroup> leaderboardGroupsByID;
/**
* See {@link #leaderboardsByNameLock}
*/
private final NamedReentrantReadWriteLock leaderboardGroupsByNameLock;
private final CompetitorStore competitorStore;
/**
* A set based on a concurrent hash map, therefore being thread safe
*/
private Set<DynamicTrackedRegatta> regattasObservedForDefaultLeaderboard = Collections
.newSetFromMap(new ConcurrentHashMap<DynamicTrackedRegatta, Boolean>());
private final MongoObjectFactory mongoObjectFactory;
private final DomainObjectFactory domainObjectFactory;
private final ConcurrentHashMap<Regatta, DynamicTrackedRegatta> regattaTrackingCache;
/**
* Protects write access transactions that do a previous read to {@link #regattaTrackingCache}; read-only access is
* already synchronized by using a concurrent hash map for {@link #regattaTrackingCache}.
*/
private final NamedReentrantReadWriteLock regattaTrackingCacheLock;
private final ConcurrentHashMap<OperationExecutionListener<RacingEventService>, OperationExecutionListener<RacingEventService>> 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;
private final RaceLogReplicator raceLogReplicator;
private final RegattaLogReplicator regattaLogReplicator;
private final RaceLogScoringReplicator raceLogScoringReplicator;
private final MediaDB mediaDB;
private final MediaLibrary mediaLibrary;
/**
* Currently valid pairs of {@link DeviceConfigurationMatcher}s and {@link DeviceConfiguration}s. The contents of
* this map is persisted and replicated. See {@link DeviceConfigurationMapImpl}.
*/
protected final DeviceConfigurationMapImpl configurationMap;
private final WindStore windStore;
private final GPSFixStore gpsFixStore;
/**
* This author should be used for server generated race log events
*/
private final AbstractLogEventAuthor raceLogEventAuthorForServer = new LogEventAuthorImpl(
RacingEventService.class.getName(), 0);
private final PolarDataService polarDataService;
/**
* Allow only one master data import at a time to avoid situation where multiple Imports override each other in
* unpredictable fashion
*/
private final DataImportLockWithProgress dataImportLock;
/**
* If this service runs in the context of an OSGi environment, the activator should {@link #setBundleContext set the
* bundle context} on this object so that service lookups become possible.
*/
private BundleContext bundleContext;
private TypeBasedServiceFinderFactory serviceFinderFactory;
/**
* The master from which this replicable is currently replicating, or <code>null</code> if this replicable is not
* currently replicated from any master.
*/
private ReplicationMasterDescriptor replicatingFromMaster;
private Set<OperationWithResultWithIdWrapper<?, ?>> operationsSentToMasterForReplication;
private ThreadLocal<Boolean> currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster = ThreadLocal
.withInitial(() -> false);
private final Set<ClassLoader> masterDataClassLoaders = new HashSet<ClassLoader>();
/**
* Constructs a {@link DomainFactory base domain factory} that uses this object's {@link #competitorStore competitor
* store} for competitor management. This base domain factory is then also used for the construction of the
* {@link DomainObjectFactory}. This constructor variant initially clears the persistent competitor collection,
* hence removes all previously persistent competitors. This is the default for testing and for backward
* compatibility with prior releases that did not support a persistent competitor collection.
*/
public RacingEventServiceImpl() {
this(true, null);
}
public RacingEventServiceImpl(WindStore windStore, GPSFixStore gpsFixStore,
TypeBasedServiceFinderFactory serviceFinderFactory) {
this(true, windStore, gpsFixStore, serviceFinderFactory);
}
void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
/**
* Like {@link #RacingEventServiceImpl()}, but allows callers to specify that the persistent competitor collection
* be cleared before the service starts.
*
* @param clearPersistentCompetitorStore
* if <code>true</code>, the {@link PersistentCompetitorStore} is created empty, with the correcponding
* database collection cleared as well. Use with caution! When used with <code>false</code>, competitors
* created and stored during previous service executions will initially be loaded.
*/
public RacingEventServiceImpl(boolean clearPersistentCompetitorStore,
TypeBasedServiceFinderFactory serviceFinderFactory) {
this(new PersistentCompetitorStore(
PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory),
clearPersistentCompetitorStore, serviceFinderFactory), null, null, serviceFinderFactory);
}
private RacingEventServiceImpl(boolean clearPersistentCompetitorStore, WindStore windStore,
GPSFixStore gpsFixStore, TypeBasedServiceFinderFactory serviceFinderFactory) {
this(new PersistentCompetitorStore(
PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory),
clearPersistentCompetitorStore, serviceFinderFactory), windStore, gpsFixStore, serviceFinderFactory);
}
private RacingEventServiceImpl(PersistentCompetitorStore persistentCompetitorStore, WindStore windStore,
GPSFixStore gpsFixStore, TypeBasedServiceFinderFactory serviceFinderFactory) {
this(persistentCompetitorStore.getDomainObjectFactory(), persistentCompetitorStore.getMongoObjectFactory(),
persistentCompetitorStore.getBaseDomainFactory(), MediaDBFactory.INSTANCE.getDefaultMediaDB(),
persistentCompetitorStore, windStore, gpsFixStore, serviceFinderFactory);
}
/**
* 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(DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory,
MediaDB mediaDB, WindStore windStore, GPSFixStore gpsFixStore) {
this(domainObjectFactory, mongoObjectFactory, domainObjectFactory.getBaseDomainFactory(), mediaDB,
domainObjectFactory.getBaseDomainFactory().getCompetitorStore(), windStore, gpsFixStore, null);
}
/**
* @param windStore
* if <code>null</code>, a default {@link MongoWindStore} will be used, based on the persistence set-up
* of this service
* @param serviceFinderFactory
* 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.
*/
private RacingEventServiceImpl(DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory,
com.sap.sailing.domain.base.DomainFactory baseDomainFactory, MediaDB mediaDb,
CompetitorStore competitorStore, WindStore windStore, GPSFixStore gpsFixStore,
TypeBasedServiceFinderFactory serviceFinderFactory) {
logger.info("Created " + this);
this.masterDataClassLoaders.add(this.getClass().getClassLoader());
this.operationsSentToMasterForReplication = new HashSet<>();
if (windStore == null) {
try {
windStore = MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
this.baseDomainFactory = baseDomainFactory;
this.domainObjectFactory = domainObjectFactory;
this.mongoObjectFactory = mongoObjectFactory;
this.mediaDB = mediaDb;
this.competitorStore = competitorStore;
this.competitorStore.addCompetitorUpdateListener(new CompetitorUpdateListener() {
@Override
public void competitorUpdated(Competitor competitor) {
replicate(new UpdateCompetitor(competitor.getId().toString(), competitor.getName(), competitor
.getColor(), competitor.getEmail(), competitor.getBoat().getSailID(), competitor.getTeam().getNationality(),
competitor.getTeam().getImage()));
}
});
this.windStore = windStore;
this.dataImportLock = new DataImportLockWithProgress();
remoteSailingServerSet = new RemoteSailingServerSet(scheduler);
regattasByName = new ConcurrentHashMap<String, Regatta>();
regattasByNameLock = new NamedReentrantReadWriteLock("regattasByName for " + this, /* fair */false);
eventsById = new ConcurrentHashMap<Serializable, Event>();
regattaTrackingCache = new ConcurrentHashMap<>();
regattaTrackingCacheLock = new NamedReentrantReadWriteLock("regattaTrackingCache for " + this, /* fair */false);
raceTrackersByRegatta = new ConcurrentHashMap<>();
raceTrackersByRegattaLock = new NamedReentrantReadWriteLock("raceTrackersByRegatta for " + this, /* fair */
false);
raceTrackersByID = new ConcurrentHashMap<>();
raceTrackersByIDLocks = new ConcurrentHashMap<>();
leaderboardGroupsByName = new ConcurrentHashMap<>();
leaderboardGroupsByID = new ConcurrentHashMap<>();
leaderboardGroupsByNameLock = new NamedReentrantReadWriteLock("leaderboardGroupsByName for " + this, /* fair */
false);
leaderboardsByName = new ConcurrentHashMap<String, Leaderboard>();
leaderboardsByNameLock = new NamedReentrantReadWriteLock("leaderboardsByName for " + this, /* fair */false);
operationExecutionListeners = new ConcurrentHashMap<>();
courseListeners = new ConcurrentHashMap<>();
persistentRegattasForRaceIDs = new ConcurrentHashMap<>();
final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3);
// TODO find out how many executors we have on server side and how to manage them
Executor polarExecutor = new ThreadPoolExecutor(/* corePoolSize */THREAD_POOL_SIZE,
/* maximumPoolSize */THREAD_POOL_SIZE,
/* keepAliveTime */60, TimeUnit.SECONDS,
/* workQueue */new LinkedBlockingQueue<Runnable>());
polarDataService = PolarDataServiceFactory.createStandardPolarDataService(polarExecutor);
this.raceLogReplicator = new RaceLogReplicator(this);
this.regattaLogReplicator = new RegattaLogReplicator(this);
this.raceLogScoringReplicator = new RaceLogScoringReplicator(this);
this.mediaLibrary = new MediaLibrary();
if (gpsFixStore == null) {
try {
gpsFixStore = MongoGPSFixStoreFactory.INSTANCE.getMongoGPSFixStore(mongoObjectFactory,
domainObjectFactory, serviceFinderFactory);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
this.gpsFixStore = gpsFixStore;
this.configurationMap = new DeviceConfigurationMapImpl();
this.serviceFinderFactory = serviceFinderFactory;
// 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 },
getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), null);
loadStoredEvents();
loadStoredRegattas();
loadRaceIDToRegattaAssociations();
loadStoredLeaderboardsAndGroups();
loadLinksFromEventsToLeaderboardGroups();
loadMediaLibary();
loadStoredDeviceConfigurations();
loadAllRemoteSailingServersAndSchedulePeriodicEventCacheRefresh();
}
@Override
public boolean isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster() {
return currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster.get();
}
@Override
public void setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(
boolean currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster) {
this.currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster
.set(currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster);
}
@Override
public PolarDataService getPolarDataService() {
return polarDataService;
}
@Override
public void clearState() throws Exception {
for (String leaderboardGroupName : new ArrayList<>(this.leaderboardGroupsByName.keySet())) {
removeLeaderboardGroup(leaderboardGroupName);
}
for (String leaderboardName : new ArrayList<>(this.leaderboardsByName.keySet())) {
removeLeaderboard(leaderboardName);
}
for (Regatta regatta : new ArrayList<>(this.regattasByName.values())) {
stopTracking(regatta);
removeRegatta(regatta);
}
for (Event event : new ArrayList<>(this.eventsById.values())) {
removeEvent(event.getId());
}
for (MediaTrack mediaTrack : this.mediaLibrary.allTracks()) {
mediaTrackDeleted(mediaTrack);
}
// TODO clear user store? See bug 2430.
this.competitorStore.clear();
// 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 },
getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), null);
}
@Override
public com.sap.sailing.domain.base.DomainFactory getBaseDomainFactory() {
return baseDomainFactory;
}
@Override
public MongoObjectFactory getMongoObjectFactory() {
return mongoObjectFactory;
}
@Override
public DomainObjectFactory getDomainObjectFactory() {
return domainObjectFactory;
}
private void loadRaceIDToRegattaAssociations() {
persistentRegattasForRaceIDs.putAll(domainObjectFactory.loadRaceIDToRegattaAssociations(this));
}
private void loadStoredRegattas() {
LockUtil.lockForWrite(regattasByNameLock);
try {
for (Regatta regatta : domainObjectFactory.loadAllRegattas(this)) {
logger.info("putting regatta " + regatta.getName() + " (" + regatta.hashCode()
+ ") into regattasByName");
regattasByName.put(regatta.getName(), regatta);
regatta.addRegattaListener(this);
onRegattaLikeAdded(regatta);
regatta.addRaceColumnListener(raceLogReplicator);
regatta.addRaceColumnListener(raceLogScoringReplicator);
}
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
}
private void loadStoredEvents() {
for (Event event : domainObjectFactory.loadAllEvents()) {
if (event.getId() != null)
eventsById.put(event.getId(), event);
}
}
private void loadLinksFromEventsToLeaderboardGroups() {
domainObjectFactory.loadLeaderboardGroupLinksForEvents(/* eventResolver */this, /* leaderboardGroupResolver */
this);
}
private void loadAllRemoteSailingServersAndSchedulePeriodicEventCacheRefresh() {
for (RemoteSailingServerReference sailingServer : domainObjectFactory.loadAllRemoteSailingServerReferences()) {
remoteSailingServerSet.add(sailingServer);
}
}
/**
* 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<MediaTrack> allDbMediaTracks = mediaDB.loadAllMediaTracks();
mediaTracksAdded(allDbMediaTracks);
}
private void loadStoredDeviceConfigurations() {
for (Entry<DeviceConfigurationMatcher, DeviceConfiguration> entry : domainObjectFactory
.loadAllDeviceConfigurations()) {
configurationMap.put(entry.getKey(), entry.getValue());
}
}
@Override
public void addLeaderboard(Leaderboard leaderboard) {
LockUtil.lockForWrite(leaderboardsByNameLock);
try {
leaderboardsByName.put(leaderboard.getName(), leaderboard);
} finally {
LockUtil.unlockAfterWrite(leaderboardsByNameLock);
}
// RaceColumns of RegattaLeaderboards are tracked via its Regatta!
if (leaderboard instanceof FlexibleLeaderboard) {
onRegattaLikeAdded(((FlexibleLeaderboard) leaderboard).getRegattaLike());
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);
LockUtil.lockForWrite(leaderboardGroupsByNameLock);
try {
leaderboardGroupsByName.put(leaderboardGroup.getName(), leaderboardGroup);
leaderboardGroupsByID.put(leaderboardGroup.getId(), leaderboardGroup);
} finally {
LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock);
}
}
// 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);
CourseArea courseArea = getCourseArea(courseAreaId);
FlexibleLeaderboard result = new FlexibleLeaderboardImpl(getRaceLogStore(), getRegattaLogStore(),
leaderboardName, new ThresholdBasedResultDiscardingRuleImpl(discardThresholds), scoringScheme,
courseArea);
result.setDisplayName(leaderboardDisplayName);
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 ThresholdBasedResultDiscardingRuleImpl(discardThresholds));
result.setDisplayName(leaderboardDisplayName);
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);
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) {
// remove race log under old identifier; the race log identifier changes
for (Fleet fleet : raceColumn.getFleets()) {
getMongoObjectFactory().removeRaceLog(raceColumn.getRaceLogIdentifier(fleet));
}
((FlexibleRaceColumn) raceColumn).setName(newColumnName);
// store the race logs again under the new identifiers
storeRaceLogs(raceColumn);
updateStoredLeaderboard(leaderboard);
} else {
throw new IllegalArgumentException("Race column " + oldColumnName + " cannot be renamed");
}
} else {
throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found");
}
}
/**
* When a race column is renamed, its race log identifiers change. Therefore, the race logs need to be stored under
* the new identifier again to be consistent with the in-memory image again.
*/
private void storeRaceLogs(RaceColumn raceColumn) {
for (Fleet fleet : raceColumn.getFleets()) {
RaceLogIdentifier identifier = raceColumn.getRaceLogIdentifier(fleet);
RaceLogEventVisitor storeVisitor = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStoreVisitor(
identifier, getMongoObjectFactory());
RaceLog raceLog = raceColumn.getRaceLog(fleet);
raceLog.lockForRead();
try {
for (RaceLogEvent e : raceLog.getRawFixes()) {
e.accept(storeVisitor);
}
} finally {
raceLog.unlockAfterRead();
}
}
}
@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) {
final Leaderboard toRename = leaderboardsByName.get(oldName);
LockUtil.lockForWrite(leaderboardsByNameLock);
try {
if (toRename == null) {
throw new IllegalArgumentException("No leaderboard with name " + oldName + " found");
}
if (leaderboardsByName.containsKey(newName)) {
throw new IllegalArgumentException("Leaderboard with name " + newName + " already exists");
}
if (toRename instanceof Renamable) {
((Renamable) toRename).setName(newName);
leaderboardsByName.remove(oldName);
leaderboardsByName.put(newName, toRename);
} else {
throw new IllegalArgumentException("Leaderboard with name " + newName + " is of type "
+ toRename.getClass().getSimpleName() + " and therefore cannot be renamed");
}
} finally {
LockUtil.unlockAfterWrite(leaderboardsByNameLock);
}
// don't need the lock anymore to update DB
if (toRename instanceof Renamable) {
mongoObjectFactory.renameLeaderboard(oldName, newName);
syncGroupsAfterLeaderboardChange(toRename, true);
}
}
@Override
public void updateStoredLeaderboard(Leaderboard leaderboard) {
getMongoObjectFactory().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;
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);
if (leaderboard != null) {
leaderboard.removeRaceColumnListener(raceLogReplicator);
leaderboard.removeRaceColumnListener(raceLogScoringReplicator);
mongoObjectFactory.removeLeaderboard(leaderboardName);
syncGroupsAfterLeaderboardRemove(leaderboardName, true);
if (leaderboard instanceof FlexibleLeaderboard) {
onRegattaLikeRemoved(((FlexibleLeaderboard) leaderboard).getRegattaLike());
}
leaderboard.destroy();
}
}
private Leaderboard removeLeaderboardFromLeaderboardsByName(String leaderboardName) {
LockUtil.lockForWrite(leaderboardsByNameLock);
try {
return leaderboardsByName.remove(leaderboardName);
} finally {
LockUtil.unlockAfterWrite(leaderboardsByNameLock);
}
}
/**
* 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;
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) {
return leaderboardsByName.get(name);
}
@Override
public Position getMarkPosition(Mark mark, LeaderboardThatHasRegattaLike leaderboard, TimePoint timePoint, RaceLog raceLog) {
GPSFixTrack<Mark, GPSFix> track = null;
// If no spanning track is found, the fix closest to the time point requested is used instead
GPSFix nonSpanningFallback = null;
for (TrackedRace trackedRace : leaderboard.getTrackedRaces()) {
GPSFixTrack<Mark, GPSFix> trackCandidate = trackedRace.getTrack(mark);
if (trackCandidate != null) {
if (spansTimePoint(trackCandidate, timePoint)) {
track = trackCandidate;
break;
} else {
nonSpanningFallback = improveTimewiseClosestFix(nonSpanningFallback, trackCandidate, timePoint);
}
}
}
if (track == null) { // no spanning track found in any tracked race, or no tracked races found
// try to load from store
DynamicGPSFixTrackImpl<Mark> loadedTrack = new DynamicGPSFixTrackImpl<Mark>(mark, 0);
track = loadedTrack;
Set<AbstractLog<?, ?>> logs = new HashSet<>();
logs.add(leaderboard.getRegattaLike().getRegattaLog());
if (raceLog == null) { // no race log explicitly provided --> use all race logs
for (RaceColumn raceColumn : leaderboard.getRaceColumns()) {
for (Fleet fleet : raceColumn.getFleets()) {
logs.add(raceColumn.getRaceLog(fleet));
}
}
} else {
logs.add(raceLog);
}
for (AbstractLog<?, ?> log : logs) {
try {
getGPSFixStore().loadMarkTrack(loadedTrack, log, mark);
} catch (Exception e) {
logger.log(Level.WARNING, "Couldn't load mark track for mark " + mark + " from log " + log, e);
}
}
}
Position result = track.getEstimatedPosition(timePoint, /* extrapolate */ false);
if (result == null) {
result = nonSpanningFallback == null ? null : nonSpanningFallback.getPosition();
}
return result;
}
private GPSFix improveTimewiseClosestFix(GPSFix nonSpanningFallback, GPSFixTrack<Mark, GPSFix> track, final TimePoint timePoint) {
GPSFix lastAtOrBefore = track.getLastFixAtOrBefore(timePoint);
GPSFix firstAtOrAfter = track.getFirstFixAtOrAfter(timePoint);
// find the fix closes to timePoint, sorting null values to the end and fixes near timePoint to the beginning
final List<GPSFix> list = Arrays.asList(nonSpanningFallback, lastAtOrBefore, firstAtOrAfter);
list.sort(new Comparator<GPSFix>() {
@Override
public int compare(GPSFix o1, GPSFix o2) {
final int result;
if (o1 == null) {
if (o2 == null) {
result = 0;
} else {
result = 1;
}
} else if (o2 == null) {
result = -1;
} else {
result = new Long(Math.abs(o1.getTimePoint().until(timePoint).asMillis())).compareTo(
Math.abs(o2.getTimePoint().until(timePoint).asMillis()));
}
return result;
}
});
return list.get(0);
}
private boolean spansTimePoint(GPSFixTrack<Mark, GPSFix> track, TimePoint timePoint) {
return track.getLastFixAtOrBefore(timePoint) != null && track.getFirstFixAtOrAfter(timePoint) != null;
}
@Override
public Map<String, Leaderboard> getLeaderboards() {
return Collections.unmodifiableMap(new HashMap<String, Leaderboard>(leaderboardsByName));
}
@Override
public Map<RemoteSailingServerReference, com.sap.sse.common.Util.Pair<Iterable<EventBase>, Exception>> getPublicEventsOfAllSailingServers() {
return remoteSailingServerSet.getCachedEventsForRemoteSailingServers(); // FIXME should probably add our own
// stuff here... Is it enough to pass on
// the remote reference URL to the
// client for leaderboard group URL
// construction?
}
@Override
public RemoteSailingServerReference addRemoteSailingServerReference(String name, URL url) {
RemoteSailingServerReference result = new RemoteSailingServerReferenceImpl(name, url);
remoteSailingServerSet.add(result);
mongoObjectFactory.storeSailingServer(result);
return result;
}
@Override
public Iterable<RemoteSailingServerReference> getLiveRemoteServerReferences() {
return remoteSailingServerSet.getLiveRemoteServerReferences();
}
@Override
public RemoteSailingServerReference getRemoteServerReferenceByName(String remoteServerReferenceName) {
return remoteSailingServerSet.getServerReferenceByName(remoteServerReferenceName);
}
@Override
public com.sap.sse.common.Util.Pair<Iterable<EventBase>, Exception> updateRemoteServerEventCacheSynchronously(
RemoteSailingServerReference ref) {
return remoteSailingServerSet.getEventsOrException(ref);
}
@Override
public void removeRemoteSailingServerReference(String name) {
remoteSailingServerSet.remove(name);
mongoObjectFactory.removeSailingServer(name);
}
@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(Regatta regattaContext, RaceDefinition r) {
Set<RaceTracker> trackers = raceTrackersByRegatta.get(regattaContext);
if (trackers != null) {
for (RaceTracker tracker : trackers) {
final Set<RaceDefinition> races = tracker.getRaces();
if (races != null && races.contains(r)) {
return true;
}
}
}
return false;
}
@Override
public Regatta getRegattaByName(String name) {
return name == null ? null : regattasByName.get(name);
}
@Override
public Regatta getOrCreateDefaultRegatta(String name, String boatClassName, Serializable id) {
Regatta result = regattasByName.get(name);
if (result == null) {
result = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), name, getBaseDomainFactory()
.getOrCreateBoatClass(boatClassName), /* startDate */null, /* endDate */null, this,
getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), id, null);
logger.info("Created default regatta " + result.getName() + " (" + hashCode() + ") on " + this);
onRegattaLikeAdded(result);
cacheAndReplicateDefaultRegatta(result);
}
return result;
}
private void onRegattaLikeAdded(IsRegattaLike isRegattaLike) {
isRegattaLike.addListener(regattaLogReplicator);
}
private void onRegattaLikeRemoved(IsRegattaLike isRegattaLike) {
isRegattaLike.removeListener(regattaLogReplicator);
getRegattaLogStore().removeRegattaLog(isRegattaLike.getRegattaLikeIdentifier());
}
@Override
public Regatta createRegatta(String fullRegattaName, String boatClassName, TimePoint startDate, TimePoint endDate,
Serializable id, Iterable<? extends Series> series, boolean persistent, ScoringScheme scoringScheme,
Serializable defaultCourseAreaId, boolean useStartTimeInference) {
com.sap.sse.common.Util.Pair<Regatta, Boolean> regattaWithCreatedFlag = getOrCreateRegattaWithoutReplication(
fullRegattaName, boatClassName, startDate, endDate, id, series, persistent, scoringScheme,
defaultCourseAreaId, useStartTimeInference);
Regatta regatta = regattaWithCreatedFlag.getA();
if (regattaWithCreatedFlag.getB()) {
onRegattaLikeAdded(regatta);
replicateSpecificRegattaWithoutRaceColumns(regatta);
}
return regatta;
}
@Override
public void addRegattaWithoutReplication(Regatta regatta) {
UUID defaultCourseAreaId = null;
if (regatta.getDefaultCourseArea() != null) {
defaultCourseAreaId = regatta.getDefaultCourseArea().getId();
}
boolean wasAdded = addAndConnectRegatta(regatta.isPersistent(), defaultCourseAreaId, regatta);
if (!wasAdded) {
logger.info("Regatta with name " + regatta.getName() + " already existed, so it hasn't been added.");
}
}
private RaceLogStore getRaceLogStore() {
return MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory);
}
private RegattaLogStore getRegattaLogStore() {
return MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory);
}
@Override
public com.sap.sse.common.Util.Pair<Regatta, Boolean> getOrCreateRegattaWithoutReplication(String fullRegattaName,
String boatClassName, TimePoint startDate, TimePoint endDate, Serializable id,
Iterable<? extends Series> series, boolean persistent, ScoringScheme scoringScheme,
Serializable defaultCourseAreaId, boolean useStartTimeInference) {
CourseArea courseArea = getCourseArea(defaultCourseAreaId);
Regatta regatta = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), fullRegattaName,
getBaseDomainFactory().getOrCreateBoatClass(boatClassName), startDate, endDate, series, persistent,
scoringScheme, id, courseArea, useStartTimeInference);
boolean wasCreated = addAndConnectRegatta(persistent, defaultCourseAreaId, regatta);
if (wasCreated) {
logger.info("Created regatta " + regatta.getName() + " (" + hashCode() + ") on " + this);
}
return new com.sap.sse.common.Util.Pair<Regatta, Boolean>(regatta, wasCreated);
}
private boolean addAndConnectRegatta(boolean persistent, Serializable defaultCourseAreaId, Regatta regatta) {
boolean wasCreated = false;
// try a quick read protected by the concurrent hash map implementation
if (!regattasByName.containsKey(regatta.getName())) {
LockUtil.lockForWrite(regattasByNameLock);
try {
// check again, now that we hold the exclusive write lock
if (!regattasByName.containsKey(regatta.getName())) {
wasCreated = true;
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);
}
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
}
if (persistent) {
updateStoredRegatta(regatta);
}
for (Event event : getAllEvents()) {
for (CourseArea eventCourseArea : event.getVenue().getCourseAreas()) {
if (defaultCourseAreaId != null && eventCourseArea.getId().equals(defaultCourseAreaId)) {
event.addRegatta(regatta);
}
}
}
return wasCreated;
}
@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));
}
private NamedReentrantReadWriteLock lockRaceTrackersById(Object trackerId) {
NamedReentrantReadWriteLock lock;
synchronized (raceTrackersByIDLocks) {
lock = raceTrackersByIDLocks.get(trackerId);
if (lock == null) {
lock = new NamedReentrantReadWriteLock("raceTrackersByIDLock for " + trackerId, /* fair */false);
raceTrackersByIDLocks.put(trackerId, lock);
}
}
LockUtil.lockForWrite(lock);
return lock;
}
/**
* @param lock
* need to pass the lock obtained from {@link #lockRaceTrackersById(Object)} because a competing thread
* may already have removed the lock from the {@link #raceTrackersByIDLocks} map
*/
private void unlockRaceTrackersById(Object trackerId, NamedReentrantReadWriteLock lock) {
LockUtil.unlockAfterWrite(lock);
synchronized (raceTrackersByIDLocks) {
raceTrackersByIDLocks.remove(trackerId);
}
}
@Override
public RaceHandle addRace(RegattaIdentifier regattaToAddTo, RaceTrackingConnectivityParameters params,
long timeoutInMilliseconds) throws Exception {
final Object trackerID = params.getTrackerID();
NamedReentrantReadWriteLock raceTrackersByIdLock = lockRaceTrackersById(trackerID);
try {
RaceTracker tracker = raceTrackersByID.get(trackerID);
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, windStore, gpsFixStore);
} else {
// use the regatta selected by the RaceIdentifier regattaToAddTo
tracker = params.createRaceTracker(regatta, this, windStore, gpsFixStore);
assert tracker.getRegatta() == regatta;
}
LockUtil.lockForWrite(raceTrackersByRegattaLock);
try {
raceTrackersByID.put(trackerID, tracker);
Set<RaceTracker> trackers = raceTrackersByRegatta.get(tracker.getRegatta());
if (trackers == null) {
trackers = Collections.newSetFromMap(new ConcurrentHashMap<RaceTracker, Boolean>());
raceTrackersByRegatta.put(tracker.getRegatta(), trackers);
}
trackers.add(tracker);
} finally {
LockUtil.unlockAfterWrite(raceTrackersByRegattaLock);
}
// TODO we assume here that the regatta name is unique which necessitates adding the boat class name to
// it in RegattaImpl constructor
String regattaName = tracker.getRegatta().getName();
Regatta regattaWithName = regattasByName.get(regattaName);
// TODO we assume here that the regatta name is unique which necessitates adding the boat class name to
// it in RegattaImpl 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
LockUtil.lockForWrite(regattasByNameLock);
try {
regattasByName.remove(regattaName);
cacheAndReplicateDefaultRegatta(tracker.getRegatta());
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
} else {
throw new RuntimeException("Internal error. Two regatta 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);
}
GPSFixStore existingTrackersGPSFixStore = tracker.getGPSFixStore();
if (!existingTrackersGPSFixStore.equals(gpsFixStore)) {
logger.warning("GPSFix store mismatch. Requested GPSFix store: " + gpsFixStore
+ ". GPSFix store in use by existing tracker: " + existingTrackersGPSFixStore);
}
}
if (timeoutInMilliseconds != -1) {
scheduleAbortTrackerAfterInitialTimeout(tracker, timeoutInMilliseconds);
}
return tracker.getRacesHandle();
} finally {
unlockRaceTrackersById(trackerID, raceTrackersByIdLock);
}
}
/**
* The regatta and all its contained {@link Regatta#getAllRaces() races} are replicated to all replicas.
*
* @param regatta
* the series of this regatta must not have any {@link Series#getRaceColumns() race columns associated
* (yet)}.
*/
private void replicateSpecificRegattaWithoutRaceColumns(Regatta regatta) {
Serializable courseAreaId = null;
if (regatta.getDefaultCourseArea() != null) {
courseAreaId = regatta.getDefaultCourseArea().getId();
}
replicate(new AddSpecificRegatta(regatta.getName(), regatta.getBoatClass() == null ? null : regatta
.getBoatClass().getName(), regatta.getStartDate(), regatta.getEndDate(), regatta.getId(),
getSeriesWithoutRaceColumnsConstructionParametersAsMap(regatta), regatta.isPersistent(),
regatta.getScoringScheme(), courseAreaId, regatta.useStartTimeInference()));
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(), s
.hasSplitFleetContiguousScoring()));
}
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) {
// try a quick read first, protected by regattasByName being a concurrent hash set
if (!regattasByName.containsKey(regatta.getName())) {
// now we need to obtain exclusive write access; in between, some other thread may have added a regatta by
// that
// name, so we need to check again:
LockUtil.lockForWrite(regattasByNameLock);
try {
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.getName(), regatta.getBoatClass() == null ? null : regatta
.getBoatClass().getName(), regatta.getStartDate(), regatta.getEndDate(), regatta.getId()));
RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier();
for (RaceDefinition race : regatta.getAllRaces()) {
replicate(new AddRaceDefinition(regattaIdentifier, race));
}
}
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
}
}
@Override
public DynamicTrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore,
GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind,
long millisecondsOverWhichToAverageSpeed) {
DynamicTrackedRegatta trackedRegatta = getOrCreateTrackedRegatta(getRegatta(raceIdentifier));
RaceDefinition race = getRace(raceIdentifier);
return trackedRegatta.createTrackedRace(race, Collections.<Sideline> emptyList(), windStore, gpsFixStore,
delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed,
/* raceDefinitionSetToUpdate */null);
}
private void ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(
DynamicTrackedRegatta trackedRegatta) {
if (regattasObservedForDefaultLeaderboard.add(trackedRegatta)) {
trackedRegatta.addRaceListener(new RaceAdditionListener());
}
}
private void stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(DynamicTrackedRegatta trackedRegatta) {
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.
*
* A {@link PolarFixCacheUpdater} is added to every race so that polar fixes are aggregated when new GPS fixes
* arrive.
*
* @author Axel Uhl (d043530)
*
*/
private class RaceAdditionListener implements RaceListener, Serializable {
private static final long serialVersionUID = 1036955460477000265L;
private final Map<TrackedRace, TrackedRaceReplicator> trackedRaceReplicators;
private final Map<TrackedRace, PolarFixCacheUpdater> polarFixCacheUpdaters;
public RaceAdditionListener() {
this.trackedRaceReplicators = new HashMap<TrackedRace, TrackedRaceReplicator>();
this.polarFixCacheUpdaters = new HashMap<TrackedRace, PolarFixCacheUpdater>();
}
@Override
public void raceRemoved(TrackedRace trackedRace) {
TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace);
if (trackedRaceReplicator != null) {
trackedRace.removeListener(trackedRaceReplicator);
}
PolarFixCacheUpdater polarFixCacheUpdater = polarFixCacheUpdaters.remove(trackedRace);
if (polarFixCacheUpdater != null) {
trackedRace.removeListener(polarFixCacheUpdater);
}
}
@Override
public void raceAdded(TrackedRace trackedRace) {
// replicate the addition of the tracked race:
CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(),
trackedRace.getGPSFixStore(), 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);
}
TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace);
trackedRaceReplicators.put(trackedRace, trackedRaceReplicator);
trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, true);
PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace);
polarFixCacheUpdaters.put(trackedRace, polarFixCacheUpdater);
trackedRace.addListener(polarFixCacheUpdater);
}
}
private class PolarFixCacheUpdater extends AbstractRaceChangeListener {
private final TrackedRace race;
public PolarFixCacheUpdater(TrackedRace race) {
this.race = race;
}
@Override
public void competitorPositionChanged(GPSFixMoving fix, Competitor item) {
polarDataService.competitorPositionChanged(fix, item, race);
}
@Override
public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) {
if (oldStatus.getStatus() == TrackedRaceStatusEnum.LOADING
&& newStatus.getStatus() != TrackedRaceStatusEnum.LOADING) {
polarDataService.raceFinishedLoading(race);
}
}
}
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 startOfTrackingChanged(TimePoint startOfTracking) {
replicate(new UpdateStartOfTracking(getRaceIdentifier(), startOfTracking));
}
@Override
public void endOfTrackingChanged(TimePoint endOfTracking) {
replicate(new UpdateEndOfTracking(getRaceIdentifier(), endOfTracking));
}
@Override
public void startTimeReceivedChanged(TimePoint startTimeReceived) {
replicate(new UpdateStartTimeReceived(getRaceIdentifier(), startTimeReceived));
}
@Override
public void startOfRaceChanged(TimePoint oldStartOfRace, TimePoint newStartOfRace) {
// no action required; the update signaled by this call is implicit; for explicit updates
// see raceTimesChanged(TimePoint, TimePoint, TimePoint).
}
@Override
public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) {
// no-op; the course change is replicated by the separate CourseChangeReplicator
}
@Override
public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) {
// no-op; the course change is replicated by the separate CourseChangeReplicator
}
@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, TrackedRaceStatus oldStatus) {
replicate(new UpdateTrackedRaceStatus(getRaceIdentifier(), newStatus));
}
@Override
public void markPositionChanged(GPSFix fix, Mark mark, boolean firstInTrack) {
final RecordMarkGPSFix operation;
if (firstInTrack) {
operation = new RecordMarkGPSFixForNewMarkTrack(getRaceIdentifier(), mark, fix);
} else {
operation = new RecordMarkGPSFixForExistingTrack(getRaceIdentifier(), mark, fix);
}
replicate(operation);
}
@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 {
final Set<RaceTracker> trackersForRegatta = raceTrackersByRegatta.get(regatta);
if (trackersForRegatta != null) {
for (RaceTracker raceTracker : trackersForRegatta) {
final Set<RaceDefinition> races = raceTracker.getRaces();
if (races != null) {
for (RaceDefinition race : races) {
stopTrackingWind(regatta, race);
}
}
raceTracker.stop(/* preemptive */false);
final Object trackerId = raceTracker.getID();
final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId);
try {
raceTrackersByID.remove(trackerId);
} finally {
unlockRaceTrackersById(trackerId, lock);
}
raceTrackersByID.remove(trackerId);
}
LockUtil.lockForWrite(raceTrackersByRegattaLock);
try {
raceTrackersByRegatta.remove(regatta);
} finally {
LockUtil.unlockAfterWrite(raceTrackersByRegattaLock);
}
}
}
@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);
LockUtil.lockForWrite(regattasByNameLock);
try {
regattasByName.remove(regatta.getName());
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
LockUtil.lockForWrite(regattaTrackingCacheLock);
try {
regattaTrackingCache.remove(regatta);
} finally {
LockUtil.unlockAfterWrite(regattaTrackingCacheLock);
}
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 tracking 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 calling {@link RaceTracker#stop(boolean) stopping}.
*
* @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 = raceTrackersByRegatta.get(regatta);
if (trackersForRegatta != null) {
trackersForRegatta.remove(tracker);
}
tracker.stop(/* preemptive */true);
final Object trackerId = tracker.getID();
final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId);
try {
raceTrackersByID.remove(trackerId);
} finally {
unlockRaceTrackersById(trackerId, lock);
}
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 + "...");
final Set<RaceTracker> trackerSet = raceTrackersByRegatta.get(regatta);
if (trackerSet != null) {
Iterator<RaceTracker> trackerIter = trackerSet.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(/* preemptive */false);
trackerIter.remove();
final Object trackerId = raceTracker.getID();
final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId);
try {
raceTrackersByID.remove(trackerId);
} finally {
unlockRaceTrackersById(trackerId, lock);
}
}
}
} else {
logger.warning("Didn't find any trackers for regatta " + regatta);
}
stopTrackingWind(regatta, race);
// if the last tracked race was removed, confirm that tracking for the entire regatta has stopped
if (trackerSet == null || trackerSet.isEmpty()) {
stopTracking(regatta);
}
}
@Override
public void removeRegatta(Regatta regatta) throws MalformedURLException, IOException, InterruptedException {
Set<RegattaLeaderboard> leaderboardsToRemove = new HashSet<>();
for (Leaderboard leaderboard : getLeaderboards().values()) {
if (leaderboard instanceof RegattaLeaderboard) {
RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) leaderboard;
if (regattaLeaderboard.getRegatta() == regatta) {
leaderboardsToRemove.add(regattaLeaderboard);
}
}
}
for (RegattaLeaderboard regattaLeaderboardToRemove : leaderboardsToRemove) {
removeLeaderboard(regattaLeaderboardToRemove.getName());
}
for (RaceDefinition race : regatta.getAllRaces()) {
removeRace(regatta, race);
mongoObjectFactory.removeRegattaForRaceID(race.getName(), regatta);
persistentRegattasForRaceIDs.remove(race.getId().toString());
}
if (regatta.isPersistent()) {
mongoObjectFactory.removeRegatta(regatta);
}
LockUtil.lockForWrite(regattasByNameLock);
try {
regattasByName.remove(regatta.getName());
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
regatta.removeRegattaListener(this);
regatta.removeRaceColumnListener(raceLogReplicator);
regatta.removeRaceColumnListener(raceLogScoringReplicator);
onRegattaLikeRemoved(regatta);
}
@Override
public void removeSeries(Series series) throws MalformedURLException, IOException, InterruptedException {
Regatta regatta = series.getRegatta();
regatta.removeSeries(series);
if (regatta.isPersistent()) {
mongoObjectFactory.storeRegatta(regatta);
}
}
@Override
public Regatta updateRegatta(RegattaIdentifier regattaIdentifier, TimePoint startDate, TimePoint endDate,
Serializable newDefaultCourseAreaId, RegattaConfiguration newRegattaConfiguration,
Iterable<? extends Series> series, boolean useStartTimeInference) {
// We're not doing any renaming of the regatta itself, therefore we don't have to sync on the maps.
Regatta regatta = getRegatta(regattaIdentifier);
CourseArea newCourseArea = getCourseArea(newDefaultCourseAreaId);
if (newCourseArea != regatta.getDefaultCourseArea()) {
regatta.setDefaultCourseArea(newCourseArea);
}
regatta.setStartDate(startDate);
regatta.setEndDate(endDate);
if (regatta.useStartTimeInference() != useStartTimeInference) {
regatta.setUseStartTimeInference(useStartTimeInference);
if (getTrackedRegatta(regatta) != null) {
for (DynamicTrackedRace trackedRace : getTrackedRegatta(regatta).getTrackedRaces()) {
// the start times of the regatta's tracked races now have to be re-evaluated the next time they are
// queried
trackedRace.invalidateStartTime();
}
}
}
regatta.setRegattaConfiguration(newRegattaConfiguration);
if (series != null) {
for (Series seriesObj : series) {
regatta.addSeries(seriesObj);
}
}
regatta.adjustEventToRegattaAssociation(this);
if (regatta.isPersistent()) {
mongoObjectFactory.storeRegatta(regatta);
}
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
for (Series series : regatta.getSeries()) {
for (RaceColumnInSeries raceColumn : series.getRaceColumns()) {
for (Fleet fleet : series.getFleets()) {
if (raceColumn.getTrackedRace(fleet) == trackedRace) {
raceColumn.releaseTrackedRace(fleet);
}
}
}
}
for (Leaderboard leaderboard : getLeaderboards().values()) {
if (leaderboard instanceof FlexibleLeaderboard) { // RegattaLeaderboards have implicitly been updated by
// the code above
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
// untouched, e.g., for later re-load
}
}
}
}
}
}
// remove the race from the (default) 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);
LockUtil.lockForWrite(regattasByNameLock);
try {
regattasByName.remove(regatta.getName());
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
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 {
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(/* preemptive */true);
// remove it from the raceTrackers by Regatta
trackerIter.remove();
final Object trackerId = raceTracker.getID();
final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId);
try {
raceTrackersByID.remove(trackerId);
} finally {
unlockRaceTrackersById(trackerId, lock);
}
// 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 Exception {
for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) {
windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination);
}
}
@Override
public void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException {
for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) {
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
if (windTracker != null) {
windTracker.stop();
}
}
}
@Override
public Iterable<com.sap.sse.common.Util.Triple<Regatta, RaceDefinition, String>> getWindTrackedRaces() {
List<com.sap.sse.common.Util.Triple<Regatta, RaceDefinition, String>> result = new ArrayList<com.sap.sse.common.Util.Triple<Regatta, RaceDefinition, String>>();
for (Regatta regatta : getAllRegattas()) {
for (RaceDefinition race : regatta.getAllRaces()) {
for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) {
WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race);
if (windTracker != null) {
result.add(new com.sap.sse.common.Util.Triple<Regatta, RaceDefinition, String>(regatta, race,
windTracker.toString()));
}
}
}
}
return result;
}
@Override
public DynamicTrackedRace getTrackedRace(Regatta regatta, RaceDefinition race) {
return getOrCreateTrackedRegatta(regatta).getTrackedRace(race);
}
private DynamicTrackedRace getExistingTrackedRace(Regatta regatta, RaceDefinition race) {
return getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race);
}
@Override
public DynamicTrackedRegatta getOrCreateTrackedRegatta(Regatta regatta) {
cacheAndReplicateDefaultRegatta(regatta);
LockUtil.lockForWrite(regattaTrackingCacheLock);
try {
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;
} finally {
LockUtil.unlockAfterWrite(regattaTrackingCacheLock);
}
}
@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");
final DynamicTrackedRegatta trackedRegatta;
LockUtil.lockForWrite(regattaTrackingCacheLock);
try {
trackedRegatta = regattaTrackingCache.remove(regatta);
} finally {
LockUtil.unlockAfterWrite(regattaTrackingCacheLock);
}
stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(trackedRegatta);
}
@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 DynamicTrackedRace getTrackedRace(RegattaAndRaceIdentifier raceIdentifier) {
DynamicTrackedRace 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 DynamicTrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) {
Regatta regatta = getRegattaByName(raceIdentifier.getRegattaName());
DynamicTrackedRace 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() {
return Collections.unmodifiableMap(new HashMap<String, LeaderboardGroup>(leaderboardGroupsByName));
}
@Override
public LeaderboardGroup getLeaderboardGroupByName(String groupName) {
return leaderboardGroupsByName.get(groupName);
}
@Override
public LeaderboardGroup getLeaderboardGroupByID(UUID leaderboardGroupID) {
return leaderboardGroupsByID.get(leaderboardGroupID);
}
@Override
public LeaderboardGroup addLeaderboardGroup(UUID id, String groupName, String description, String displayName,
boolean displayGroupsInReverseOrder, List<String> leaderboardNames,
int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) {
ArrayList<Leaderboard> leaderboards = new ArrayList<>();
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(id, groupName, description, displayName,
displayGroupsInReverseOrder, leaderboards);
if (overallLeaderboardScoringSchemeType != null) {
// create overall leaderboard and its discards settings
addOverallLeaderboardToLeaderboardGroup(result,
getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType),
overallLeaderboardDiscardThresholds);
}
LockUtil.lockForWrite(leaderboardGroupsByNameLock);
try {
if (leaderboardGroupsByName.containsKey(groupName)) {
throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists");
}
leaderboardGroupsByName.put(groupName, result);
leaderboardGroupsByID.put(result.getId(), result);
} finally {
LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock);
}
mongoObjectFactory.storeLeaderboardGroup(result);
return result;
}
@Override
public void addLeaderboardGroupWithoutReplication(LeaderboardGroup leaderboardGroup) {
LockUtil.lockForWrite(leaderboardGroupsByNameLock);
try {
String groupName = leaderboardGroup.getName();
if (leaderboardGroupsByName.containsKey(groupName)) {
throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists");
}
leaderboardGroupsByName.put(groupName, leaderboardGroup);
leaderboardGroupsByID.put(leaderboardGroup.getId(), leaderboardGroup);
} finally {
LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock);
}
if (leaderboardGroup.hasOverallLeaderboard()) {
addLeaderboard(leaderboardGroup.getOverallLeaderboard());
}
mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup);
}
@Override
public void removeLeaderboardGroup(String groupName) {
final LeaderboardGroup leaderboardGroup;
LockUtil.lockForWrite(leaderboardGroupsByNameLock);
try {
leaderboardGroup = leaderboardGroupsByName.remove(groupName);
if (leaderboardGroup != null) {
leaderboardGroupsByID.remove(leaderboardGroup.getId());
}
} finally {
LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock);
}
mongoObjectFactory.removeLeaderboardGroup(groupName);
if (leaderboardGroup != null && leaderboardGroup.getOverallLeaderboard() != null) {
removeLeaderboard(leaderboardGroup.getOverallLeaderboard().getName());
}
}
@Override
public void renameLeaderboardGroup(String oldName, String newName) {
LockUtil.lockForWrite(leaderboardGroupsByNameLock);
try {
final LeaderboardGroup toRename = leaderboardGroupsByName.get(oldName);
if (toRename == null) {
throw new IllegalArgumentException("No leaderboard group with name " + oldName + " found");
}
if (leaderboardGroupsByName.containsKey(newName)) {
throw new IllegalArgumentException("Leaderboard group with name " + newName + " already exists");
}
leaderboardGroupsByName.remove(oldName);
toRename.setName(newName);
leaderboardGroupsByName.put(newName, toRename);
} finally {
LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock);
}
mongoObjectFactory.renameLeaderboardGroup(oldName, newName);
}
@Override
public void updateLeaderboardGroup(String oldName, String newName, String description, String displayName,
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);
}
if (!Util.equalsWithNull(displayName, group.getDisplayName())) {
group.setDisplayName(displayName);
}
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);
}
private ScheduledExecutorService getScheduler() {
return scheduler;
}
@Override
public ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is) throws IOException {
return getBaseDomainFactory().createObjectInputStreamResolvingAgainstThisFactory(is);
}
@Override
public Serializable getId() {
return getClass().getName();
}
@Override
public Iterable<OperationExecutionListener<RacingEventService>> getOperationExecutionListeners() {
return operationExecutionListeners.keySet();
}
@Override
public void addOperationExecutionListener(OperationExecutionListener<RacingEventService> listener) {
operationExecutionListeners.put(listener, listener);
}
@Override
public void removeOperationExecutionListener(OperationExecutionListener<RacingEventService> listener) {
operationExecutionListeners.remove(listener);
}
@Override
public void serializeForInitialReplicationInternal(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("Serializing persisted competitors...");
oos.writeObject(competitorStore);
logoutput.append("Serialized " + competitorStore.size() + " persisted competitors\n");
logger.info("Serializing configuration map...");
oos.writeObject(configurationMap);
logoutput.append("Serialized " + configurationMap.size() + " configuration entries\n");
for (DeviceConfigurationMatcher matcher : configurationMap.keySet()) {
logoutput.append(String.format("%3s\n", matcher.toString()));
}
logger.info("Serializing remote sailing server references...");
final ArrayList<RemoteSailingServerReference> remoteServerReferences = new ArrayList<>(remoteSailingServerSet
.getCachedEventsForRemoteSailingServers().keySet());
oos.writeObject(remoteServerReferences);
logoutput.append("Serialized " + remoteServerReferences.size() + " remote sailing server references\n");
logger.info(logoutput.toString());
}
@SuppressWarnings("unchecked")
// all the casts of ois.readObject()'s return value to Map<..., ...>
// the type-parameters in the casts of the de-serialized collection objects can't be checked
@Override
public void initiallyFillFromInternal(ObjectInputStream ois) throws IOException, ClassNotFoundException,
InterruptedException {
logger.info("Performing initial replication load on " + this);
// 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
StringBuffer logoutput = new StringBuffer();
logger.info("Reading all events...");
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()));
}
logger.info("Reading all regattas...");
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
logger.info("Reading all dynamic tracked regattas...");
for (DynamicTrackedRegatta trackedRegattaToObserve : (Set<DynamicTrackedRegatta>) ois.readObject()) {
ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(trackedRegattaToObserve);
}
logger.info("Reading all of the regatta tracking cache...");
regattaTrackingCache.putAll((Map<Regatta, DynamicTrackedRegatta>) ois.readObject());
logoutput.append("Received " + regattaTrackingCache.size() + " NEW regatta tracking cache entries\n");
logger.info("Reading leaderboard groups...");
leaderboardGroupsByName.putAll((Map<String, LeaderboardGroup>) ois.readObject());
logoutput.append("Received " + leaderboardGroupsByName.size() + " NEW leaderboard groups\n");
for (LeaderboardGroup lg : leaderboardGroupsByName.values()) {
leaderboardGroupsByID.put(lg.getId(), lg);
logoutput.append(String.format("%3s\n", lg.toString()));
}
logger.info("Reading leaderboards by name...");
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();
} else if (leaderboard instanceof FlexibleLeaderboard) {
// and re-establish the RaceLogReplicator as listener on FlexibleLeaderboard objects
leaderboard.addRaceColumnListener(raceLogReplicator);
}
}
logger.info("Reading media library...");
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()));
}
// only copy the competitors from the deserialized competitor store; don't use it because it will have set
// a default Mongo object factory
logger.info("Reading competitors...");
for (Competitor competitor : ((CompetitorStore) ois.readObject()).getCompetitors()) {
DynamicCompetitor dynamicCompetitor = (DynamicCompetitor) competitor;
competitorStore.getOrCreateCompetitor(dynamicCompetitor.getId(), dynamicCompetitor.getName(),
dynamicCompetitor.getColor(), dynamicCompetitor.getEmail(), dynamicCompetitor.getTeam(), dynamicCompetitor.getBoat());
}
logoutput.append("Received " + competitorStore.size() + " NEW competitors\n");
logger.info("Reading device configurations...");
configurationMap.putAll((DeviceConfigurationMapImpl) ois.readObject());
logoutput.append("Received " + configurationMap.size() + " NEW configuration entries\n");
for (DeviceConfigurationMatcher matcher : configurationMap.keySet()) {
logoutput.append(String.format("%3s\n", matcher.toString()));
}
logger.info("Reading remote sailing server references...");
for (RemoteSailingServerReference remoteSailingServerReference : (Iterable<RemoteSailingServerReference>) ois
.readObject()) {
remoteSailingServerSet.add(remoteSailingServerReference);
logoutput.append("Received remote sailing server reference " + remoteSailingServerReference);
}
// make sure to initialize listeners correctly
for (Regatta regatta : regattasByName.values()) {
RegattaImpl regattaImpl = (RegattaImpl) regatta;
regattaImpl.initializeSeriesAfterDeserialize();
regattaImpl.addRaceColumnListener(raceLogReplicator);
}
logger.info(logoutput.toString());
}
@Override
public void clearReplicaState() throws MalformedURLException, IOException, InterruptedException {
logger.info("Clearing all data structures...");
LockUtil.lockForWrite(regattasByNameLock);
try {
regattasByName.clear();
} finally {
LockUtil.unlockAfterWrite(regattasByNameLock);
}
regattasObservedForDefaultLeaderboard.clear();
if (raceTrackersByRegatta != null && !raceTrackersByRegatta.isEmpty()) {
for (DynamicTrackedRegatta regatta : regattaTrackingCache.values()) {
final Set<RaceTracker> trackers = raceTrackersByRegatta.get(regatta.getRegatta());
if (trackers != null) {
for (RaceTracker tracker : trackers) {
tracker.stop(/* preemptive */true);
}
}
}
}
LockUtil.lockForWrite(regattaTrackingCacheLock);
try {
regattaTrackingCache.clear();
} finally {
LockUtil.unlockAfterWrite(regattaTrackingCacheLock);
}
LockUtil.lockForWrite(raceTrackersByRegattaLock);
try {
raceTrackersByRegatta.clear();
} finally {
LockUtil.unlockAfterWrite(raceTrackersByRegattaLock);
}
LockUtil.lockForWrite(leaderboardGroupsByNameLock);
try {
leaderboardGroupsByName.clear();
leaderboardGroupsByID.clear();
} finally {
LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock);
}
LockUtil.lockForWrite(leaderboardsByNameLock);
try {
leaderboardsByName.clear();
} finally {
LockUtil.unlockAfterWrite(leaderboardsByNameLock);
}
eventsById.clear();
mediaLibrary.clear();
competitorStore.clear();
remoteSailingServerSet.clear();
}
// Used for TESTING only
@Override
public Event addEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate,
String venue, boolean isPublic, UUID id) {
Event result = createEventWithoutReplication(eventName, eventDescription, startDate, endDate, venue, isPublic,
id, /* imageURLs */Collections.<URL> emptyList(),
/* videoURLs */Collections.<URL> emptyList(), /* sponsorImageURLs */Collections.<URL> emptyList(), /* logoImageURL */
null, /* officialWebsiteURL */null);
replicate(new CreateEvent(eventName, eventDescription, startDate, endDate, venue, isPublic, /* imageURLs */
id, Collections.<URL> emptyList(),
/* videoURLs */Collections.<URL> emptyList(), /* sponsorImageURLs */Collections.<URL> emptyList(),
/* logoimageURL */null, /* officialWebsiteURLAsString */null));
return result;
}
@Override
public void addEventWithoutReplication(Event event) {
addEvent(event);
}
@Override
public Event createEventWithoutReplication(String eventName, String eventDescription, TimePoint startDate,
TimePoint endDate, String venue, boolean isPublic, UUID id, Iterable<URL> imageURLs,
Iterable<URL> videoURLs, Iterable<URL> sponsorImageURLs, URL logoImageURL, URL officialWebsiteURL) {
Event result = new EventImpl(eventName, startDate, endDate, venue, isPublic, id);
addEvent(result);
result.setDescription(eventDescription);
result.setImageURLs(imageURLs);
result.setVideoURLs(videoURLs);
result.setSponsorImageURLs(sponsorImageURLs);
result.setLogoImageURL(logoImageURL);
result.setOfficialWebsiteURL(officialWebsiteURL);
return result;
}
private void addEvent(Event result) {
if (eventsById.containsKey(result.getId())) {
throw new IllegalArgumentException("Event with ID " + result.getId()
+ " already exists which is pretty surprising...");
}
eventsById.put(result.getId(), result);
mongoObjectFactory.storeEvent(result);
}
@Override
public void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate,
String venueName, boolean isPublic, Iterable<UUID> leaderboardGroupIds, URL officialWebsiteURL,
URL logoImageURL, Iterable<URL> imageURLs, Iterable<URL> videoURLs, Iterable<URL> sponsorImageURLs) {
final Event event = eventsById.get(id);
if (event == null) {
throw new IllegalArgumentException("Sailing event with ID " + id + " does not exist.");
}
event.setName(eventName);
event.setDescription(eventDescription);
event.setStartDate(startDate);
event.setEndDate(endDate);
event.setPublic(isPublic);
event.getVenue().setName(venueName);
List<LeaderboardGroup> leaderboardGroups = new ArrayList<>();
for (UUID lgid : leaderboardGroupIds) {
LeaderboardGroup lg = getLeaderboardGroupByID(lgid);
if (lg != null) {
leaderboardGroups.add(lg);
} else {
logger.info("Couldn't find leaderboard group with ID " + lgid + " while updating event "
+ event.getName());
}
}
event.setLeaderboardGroups(leaderboardGroups);
event.setOfficialWebsiteURL(officialWebsiteURL);
event.setLogoImageURL(logoImageURL);
event.setImageURLs(imageURLs);
event.setVideoURLs(videoURLs);
event.setSponsorImageURLs(sponsorImageURLs);
// TODO consider use diffutils to compute diff between old and new leaderboard groups list and apply the patch
// to keep changes minimial
mongoObjectFactory.storeEvent(event);
}
@Override
public void renameEvent(UUID id, String newName) {
final Event toRename = eventsById.get(id);
if (toRename == null) {
throw new IllegalArgumentException("No sailing event with ID " + id + " found.");
}
toRename.setName(newName);
mongoObjectFactory.renameEvent(id, newName);
replicate(new RenameEvent(id, newName));
}
@Override
public void removeEvent(UUID id) {
removeEventFromEventsById(id);
mongoObjectFactory.removeEvent(id);
replicate(new RemoveEvent(id));
}
protected void removeEventFromEventsById(Serializable id) {
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) {
setRegattaForRace(regatta, race.getId().toString());
}
@Override
public void setRegattaForRace(Regatta regatta, String raceIdAsString) {
persistentRegattasForRaceIDs.put(raceIdAsString, regatta);
mongoObjectFactory.storeRegattaForRaceID(raceIdAsString, regatta);
}
@Override
public CourseArea addCourseArea(UUID eventId, String courseAreaName, UUID courseAreaId) {
CourseArea courseArea = getBaseDomainFactory().getOrCreateCourseArea(courseAreaId, courseAreaName);
addCourseAreaWithoutReplication(eventId, courseAreaId, courseAreaName);
replicate(new AddCourseArea(eventId, courseAreaName, courseAreaId));
return courseArea;
}
@Override
public CourseArea addCourseAreaWithoutReplication(UUID eventId, UUID courseAreaId, String courseAreaName) {
final CourseArea courseArea = getBaseDomainFactory().getOrCreateCourseArea(courseAreaId, courseAreaName);
final Event event = eventsById.get(eventId);
if (event == null) {
throw new IllegalArgumentException("No sailing event with ID " + eventId + " found.");
}
event.getVenue().addCourseArea(courseArea);
mongoObjectFactory.storeEvent(event);
return courseArea;
}
@Override
public CourseArea removeCourseAreaWithoutReplication(UUID eventId, UUID courseAreaId) {
final CourseArea courseArea = getBaseDomainFactory().getExistingCourseAreaById(courseAreaId);
if (courseArea == null) {
throw new IllegalArgumentException("No course area with ID " + courseAreaId + " found.");
}
final Event event = eventsById.get(eventId);
if (event == null) {
throw new IllegalArgumentException("No sailing event with ID " + eventId + " found.");
}
event.getVenue().removeCourseArea(courseArea);
mongoObjectFactory.storeEvent(event);
return courseArea;
}
@Override
public void mediaTrackAdded(MediaTrack mediaTrack) {
if (mediaTrack.dbId == null) {
mediaTrack.dbId = mediaDB.insertMediaTrack(mediaTrack.title, mediaTrack.url, mediaTrack.startTime,
mediaTrack.duration, mediaTrack.mimeType, mediaTrack.assignedRaces);
}
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.duration);
mediaLibrary.durationChanged(mediaTrack);
replicate(new UpdateMediaTrackDurationOperation(mediaTrack));
}
@Override
public void mediaTrackAssignedRacesChanged(MediaTrack mediaTrack) {
mediaDB.updateRace(mediaTrack.dbId, mediaTrack.assignedRaces);
mediaLibrary.assignedRacesChanged(mediaTrack);
replicate(new UpdateMediaTrackRacesOperation(mediaTrack));
}
@Override
public void mediaTrackDeleted(MediaTrack mediaTrack) {
mediaDB.deleteMediaTrack(mediaTrack.dbId);
mediaLibrary.deleteMediaTrack(mediaTrack);
replicate(new RemoveMediaTrackOperation(mediaTrack));
}
@Override
public void mediaTracksImported(Collection<MediaTrack> mediaTracksToImport, boolean override) {
for (MediaTrack trackToImport : mediaTracksToImport) {
MediaTrack existingTrack = mediaLibrary.lookupMediaTrack(trackToImport);
if (existingTrack == null) {
mediaDB.insertMediaTrackWithId(trackToImport.dbId, trackToImport.title, trackToImport.url,
trackToImport.startTime, trackToImport.duration, trackToImport.mimeType,
trackToImport.assignedRaces);
mediaTrackAdded(trackToImport);
} else if (override) {
// Using fine-grained update methods.
// Rationale: Changes on more than one track property are rare
// and don't justify the introduction of a new set
// of methods (including replication).
if (!Util.equalsWithNull(existingTrack.title, trackToImport.title)) {
mediaTrackTitleChanged(trackToImport);
}
if (!Util.equalsWithNull(existingTrack.url, trackToImport.url)) {
mediaTrackUrlChanged(trackToImport);
}
if (!Util.equalsWithNull(existingTrack.startTime, trackToImport.startTime)) {
mediaTrackStartTimeChanged(trackToImport);
}
if (!Util.equalsWithNull(existingTrack.duration, trackToImport.duration)) {
mediaTrackDurationChanged(trackToImport);
}
if (!Util.equalsWithNull(existingTrack.assignedRaces, trackToImport.assignedRaces)) {
mediaTrackAssignedRacesChanged(trackToImport);
}
}
}
}
@Override
public Collection<MediaTrack> getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) {
return mediaLibrary.findMediaTracksForRace(regattaAndRaceIdentifier);
}
@Override
public Collection<MediaTrack> getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier) {
TrackedRace trackedRace = getExistingTrackedRace(regattaAndRaceIdentifier);
if (trackedRace != null) {
if (trackedRace.isLive(MillisecondsTimePoint.now())) {
return mediaLibrary.findLiveMediaTracks();
} else {
TimePoint raceStart = trackedRace.getStartOfRace() == null ? trackedRace.getStartOfTracking()
: trackedRace.getStartOfRace();
TimePoint raceEnd = trackedRace.getEndOfRace() == null ? trackedRace.getEndOfTracking() : trackedRace
.getEndOfRace();
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();
}
@Override
public void reloadRaceLog(String leaderboardName, String raceColumnName, String fleetName) {
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
if (leaderboard != null) {
RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName);
if (raceColumn != null) {
Fleet fleetImpl = raceColumn.getFleetByName(fleetName);
RaceLog racelog = raceColumn.getRaceLog(fleetImpl);
if (racelog != null) {
raceColumn.reloadRaceLog(fleetImpl);
logger.info("Reloaded race log for fleet " + fleetImpl + " for race column " + raceColumn.getName()
+ " for leaderboard " + leaderboard.getName());
}
}
}
}
@Override
public ConcurrentHashMap<String, Regatta> getPersistentRegattasForRaceIDs() {
return persistentRegattasForRaceIDs;
}
@Override
public WindStore getWindStore() {
return windStore;
}
@Override
public DeviceConfiguration getDeviceConfiguration(DeviceConfigurationIdentifier identifier) {
return configurationMap.getByMatch(identifier);
}
@Override
public void createOrUpdateDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) {
configurationMap.put(matcher, configuration);
mongoObjectFactory.storeDeviceConfiguration(matcher, configuration);
replicate(new CreateOrUpdateDeviceConfiguration(matcher, configuration));
}
@Override
public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) {
configurationMap.remove(matcher);
mongoObjectFactory.removeDeviceConfiguration(matcher);
replicate(new RemoveDeviceConfiguration(matcher));
}
@Override
public Map<DeviceConfigurationMatcher, DeviceConfiguration> getAllDeviceConfigurations() {
return new HashMap<DeviceConfigurationMatcher, DeviceConfiguration>(configurationMap);
}
@Override
public TimePoint setStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName,
String authorName, int authorPriority, int passId, TimePoint logicalTimePoint, TimePoint startTime,
RacingProcedureType racingProcedure) {
RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName);
if (raceLog != null) {
RaceState state = RaceStateImpl.create(raceLog, new LogEventAuthorImpl(authorName, authorPriority));
if (passId > raceLog.getCurrentPassId()) {
state.setAdvancePass(logicalTimePoint);
}
state.setRacingProcedure(logicalTimePoint, racingProcedure);
state.forceNewStartTime(logicalTimePoint, startTime);
return state.getStartTime();
}
return null;
}
public RaceLog getRaceLog(String leaderboardName, String raceColumnName, String fleetName) {
Leaderboard leaderboard = getLeaderboardByName(leaderboardName);
if (leaderboard != null) {
RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName);
if (raceColumn != null) {
Fleet fleetImpl = raceColumn.getFleetByName(fleetName);
return raceColumn.getRaceLog(fleetImpl);
}
}
return null;
}
@Override
public com.sap.sse.common.Util.Triple<TimePoint, Integer, RacingProcedureType> getStartTimeAndProcedure(
String leaderboardName, String raceColumnName, String fleetName) {
RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName);
if (raceLog != null) {
ReadonlyRaceState state = ReadonlyRaceStateImpl.create(raceLog);
return new com.sap.sse.common.Util.Triple<TimePoint, Integer, RacingProcedureType>(state.getStartTime(),
raceLog.getCurrentPassId(), state.getRacingProcedure().getType());
}
return null;
}
private Iterable<WindTrackerFactory> getWindTrackerFactories() {
final Set<WindTrackerFactory> result;
if (bundleContext == null) {
result = Collections.singleton((WindTrackerFactory) ExpeditionWindTrackerFactory.getInstance());
} else {
ServiceTracker<WindTrackerFactory, WindTrackerFactory> tracker = new ServiceTracker<WindTrackerFactory, WindTrackerFactory>(
bundleContext, WindTrackerFactory.class.getName(), null);
tracker.open();
result = new HashSet<>();
for (WindTrackerFactory factory : tracker.getServices(new WindTrackerFactory[0])) {
result.add(factory);
}
}
return result;
}
@Override
public GPSFixStore getGPSFixStore() {
return gpsFixStore;
}
@Override
public RaceTracker getRaceTrackerById(Object id) {
return raceTrackersByID.get(id);
}
@Override
public AbstractLogEventAuthor getServerAuthor() {
return raceLogEventAuthorForServer;
}
@Override
public CompetitorStore getCompetitorStore() {
return competitorStore;
}
@Override
public TypeBasedServiceFinderFactory getTypeBasedServiceFinderFactory() {
return serviceFinderFactory;
}
@Override
public DataImportLockWithProgress getDataImportLock() {
return dataImportLock;
}
@Override
public DataImportProgress createOrUpdateDataImportProgressWithReplication(UUID importOperationId,
double overallProgressPct, String subProgressName, double subProgressPct) {
// Create/Update locally
DataImportProgress progress = createOrUpdateDataImportProgressWithoutReplication(importOperationId,
overallProgressPct, subProgressName, subProgressPct);
// Create/Update on replicas
replicate(new CreateOrUpdateDataImportProgress(importOperationId, overallProgressPct, subProgressName,
subProgressPct));
return progress;
}
@Override
public DataImportProgress createOrUpdateDataImportProgressWithoutReplication(UUID importOperationId,
double overallProgressPct, String subProgressName, double subProgressPct) {
DataImportProgress progress = dataImportLock.getProgress(importOperationId);
boolean newObject = false;
if (progress == null) {
progress = new DataImportProgressImpl(importOperationId);
newObject = true;
}
progress.setOverAllProgressPct(overallProgressPct);
progress.setNameOfCurrentSubProgress(subProgressName);
progress.setCurrentSubProgressPct(subProgressPct);
if (newObject) {
dataImportLock.addProgress(importOperationId, progress);
}
return progress;
}
@Override
public void setDataImportFailedWithoutReplication(UUID importOperationId, String errorMessage) {
DataImportProgress progress = dataImportLock.getProgress(importOperationId);
if (progress != null) {
progress.setFailed();
progress.setErrorMessage(errorMessage);
}
}
@Override
public void setDataImportFailedWithReplication(UUID importOperationId, String errorMessage) {
setDataImportFailedWithoutReplication(importOperationId, errorMessage);
replicate(new DataImportFailed(importOperationId, errorMessage));
}
@Override
public void setDataImportDeleteProgressFromMapTimerWithReplication(UUID importOperationId) {
setDataImportDeleteProgressFromMapTimerWithoutReplication(importOperationId);
replicate(new SetDataImportDeleteProgressFromMapTimer(importOperationId));
}
@Override
public void setDataImportDeleteProgressFromMapTimerWithoutReplication(UUID importOperationId) {
dataImportLock.setDeleteFromMapTimer(importOperationId);
}
@Override
public Result<LeaderboardSearchResult> search(KeywordQuery query) {
long start = System.currentTimeMillis();
logger.info("Searching local server for " + query);
Result<LeaderboardSearchResult> result = new RegattaByKeywordSearchService().search(this, query);
logger.fine("Search for " + query + " took " + (System.currentTimeMillis() - start) + "ms");
return result;
}
@Override
public Result<LeaderboardSearchResultBase> searchRemotely(String remoteServerReferenceName, KeywordQuery query) {
long start = System.currentTimeMillis();
ResultImpl<LeaderboardSearchResultBase> result = null;
RemoteSailingServerReference remoteRef = remoteSailingServerSet
.getServerReferenceByName(remoteServerReferenceName);
if (remoteRef == null) {
result = null;
} else {
BufferedReader bufferedReader = null;
try {
try {
final URL eventsURL = new URL(remoteRef.getURL(), "sailingserver/api/v1/search?q="
+ URLEncoder.encode(query.toString(), "UTF-8"));
logger.info("Searching remote server " + remoteRef + " for " + query);
URLConnection urlConnection = eventsURL.openConnection();
urlConnection.connect();
bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
JSONParser parser = new JSONParser();
Object eventsAsObject = parser.parse(bufferedReader);
final LeaderboardGroupBaseJsonDeserializer leaderboardGroupBaseJsonDeserializer = new LeaderboardGroupBaseJsonDeserializer();
LeaderboardSearchResultBaseJsonDeserializer deserializer = new LeaderboardSearchResultBaseJsonDeserializer(
new EventBaseJsonDeserializer(new VenueJsonDeserializer(new CourseAreaJsonDeserializer(
DomainFactory.INSTANCE)), leaderboardGroupBaseJsonDeserializer),
leaderboardGroupBaseJsonDeserializer);
result = new ResultImpl<LeaderboardSearchResultBase>(query,
new LeaderboardSearchResultBaseRanker<LeaderboardSearchResultBase>());
JSONArray hitsAsJsonArray = (JSONArray) eventsAsObject;
for (Object hitAsObject : hitsAsJsonArray) {
JSONObject hitAsJson = (JSONObject) hitAsObject;
LeaderboardSearchResultBase hit = deserializer.deserialize(hitAsJson);
result.addHit(hit);
}
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
} catch (IOException | ParseException e) {
logger.log(Level.INFO,
"Exception trying to fetch events from remote server " + remoteRef + ": " + e.getMessage(), e);
}
}
logger.fine("Remote search on " + remoteRef + " for " + query + " took " + (System.currentTimeMillis() - start)
+ "ms");
return result;
}
@Override
public ReplicationMasterDescriptor getMasterDescriptor() {
return replicatingFromMaster;
}
@Override
public void startedReplicatingFrom(ReplicationMasterDescriptor master) {
this.replicatingFromMaster = master;
}
@Override
public void stoppedReplicatingFrom(ReplicationMasterDescriptor master) {
this.replicatingFromMaster = null;
}
@Override
public void addOperationSentToMasterForReplication(
OperationWithResultWithIdWrapper<RacingEventService, ?> operationWithResultWithIdWrapper) {
this.operationsSentToMasterForReplication.add(operationWithResultWithIdWrapper);
}
@Override
public boolean hasSentOperationToMaster(OperationWithResult<RacingEventService, ?> operation) {
return this.operationsSentToMasterForReplication.remove(operation);
}
@Override
public FileStorageManagementService getFileStorageManagementService() {
ServiceReference<FileStorageManagementService> ref = bundleContext
.getServiceReference(FileStorageManagementService.class);
if (ref == null) {
logger.warning("No file storage management service registered");
return null;
}
return bundleContext.getService(ref);
}
public void addMasterDataClassLoader(ClassLoader classLoader) {
masterDataClassLoaders.add(classLoader);
}
public void removeMasterDataClassLoader(ClassLoader classLoader) {
masterDataClassLoaders.remove(classLoader);
}
@Override
public ClassLoader getCombinedMasterDataClassLoader() {
JoinedClassLoader joinedClassLoader = new JoinedClassLoader(masterDataClassLoaders);
return joinedClassLoader;
}
}