bug6275: remote server course areas loading ui

This commit is contained in:
Masha Kashirina
2026-07-21 11:53:24 +02:00
parent 482ecd04cc
commit d61f68c1dd
14 changed files with 149 additions and 8 deletions
@@ -12,6 +12,8 @@ import java.util.UUID;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.Grid;
@@ -60,6 +62,7 @@ public abstract class EventDialog extends DataEntryDialogWithDateTimeBox<EventDT
protected ExternalLinksComposite externalLinksComposite;
private final FileStorageServiceConnectionTestObservable storageServiceAvailable;
private final List<EventDTO> existingEvents;
private SailingServiceWriteAsync sailingServiceWrite;
protected static class EventParameterValidator implements Validator<EventDTO> {
@@ -131,6 +134,7 @@ public abstract class EventDialog extends DataEntryDialogWithDateTimeBox<EventDT
super(stringMessages.event(), null, stringMessages.ok(), stringMessages.cancel(), validator, callback);
this.ensureDebugId("eventDialog");
this.storageServiceAvailable = new FileStorageServiceConnectionTestObservable(sailingServiceWrite);
this.sailingServiceWrite = sailingServiceWrite;
this.stringMessages = stringMessages;
this.existingEvents = new ArrayList<>(existingEvents);
this.availableLeaderboardGroupsByName = new HashMap<>();
@@ -268,11 +272,58 @@ public abstract class EventDialog extends DataEntryDialogWithDateTimeBox<EventDT
}
}
});
final HorizontalPanel copyFromPanel = new HorizontalPanel();
copyFromPanel.setSpacing(3);
copyFromPanel.add(new Label(stringMessages.copyCourseAreasFromEvent()));
copyFromPanel.add(copyFromEventDropDown);
courseAreasPanel.add(copyFromPanel);
final HorizontalPanel localPanel = new HorizontalPanel();
localPanel.setSpacing(3);
localPanel.add(new Label(stringMessages.copyCourseAreasFromEvent()));
localPanel.add(copyFromEventDropDown);
final TextBox remoteBaseUrlBox = new TextBox();
remoteBaseUrlBox.setValue("https://www.sapsailing.com");
remoteBaseUrlBox.setVisibleLength(40);
final ListBox remoteEventDropDown = createListBox(false);
// single-element array to hold the loaded remote event data so the change handler can access it without a second RPC call
final List<Map<String, List<CourseAreaDTO>>> remoteEventsCache = new ArrayList<>(Collections.singletonList((Map<String, List<CourseAreaDTO>>) null));
final Button loadRemoteEventsButton = new Button(stringMessages.loadRemoteEvents());
loadRemoteEventsButton.addClickHandler(e -> {
remoteEventDropDown.clear();
remoteEventDropDown.addItem(stringMessages.pleaseSelect());
remoteEventsCache.set(0, null);
sailingServiceWrite.getRemoteEventNamesAndCourseAreas(remoteBaseUrlBox.getValue(),
new AsyncCallback<java.util.Map<String, List<CourseAreaDTO>>>() {
@Override
public void onSuccess(final java.util.Map<String, List<CourseAreaDTO>> result) {
remoteEventsCache.set(0, result);
for (final String eventName : result.keySet()) {
remoteEventDropDown.addItem(eventName, eventName);
}
}
@Override
public void onFailure(final Throwable caught) {
}
});
});
remoteEventDropDown.addChangeHandler(e -> {
final int selectedIndex = remoteEventDropDown.getSelectedIndex();
if (selectedIndex > 0 && remoteEventsCache.get(0) != null) {
final String selectedEventName = remoteEventDropDown.getValue(selectedIndex);
final List<CourseAreaDTO> courseAreas = remoteEventsCache.get(0).get(selectedEventName);
if (courseAreas != null) {
// copy with fresh UUIDs so these become independent course areas of this event
final List<CourseAreaDTO> copies = new ArrayList<>();
for (final CourseAreaDTO area : courseAreas) {
copies.add(new CourseAreaDTO(UUID.randomUUID(), area.getName(), area.getCenterPosition(), area.getRadius()));
}
courseAreaNameList.setValue(copies);
}
}
});
final HorizontalPanel remotePanel = new HorizontalPanel();
remotePanel.setSpacing(3);
remotePanel.add(new Label(stringMessages.copyCourseAreasFromRemoteServer()));
remotePanel.add(remoteBaseUrlBox);
remotePanel.add(loadRemoteEventsButton);
remotePanel.add(remoteEventDropDown);
courseAreasPanel.add(localPanel);
courseAreasPanel.add(remotePanel);
courseAreasPanel.add(courseAreaNameList);
final ScrollPanel courseAreasTab = new ScrollPanel(courseAreasPanel);
courseAreasTab.ensureDebugId("CourseAreasTab");
@@ -449,6 +449,8 @@ public interface SailingServiceWrite extends FileStorageManagementGwtService, Sa
RemoteSailingServerReferenceDTO getCompleteRemoteServerReference(String sailingServerName)
throws UnauthorizedException, Exception;
Map<String, List<CourseAreaDTO>> getRemoteEventNamesAndCourseAreas(String baseUrl) throws Exception;
void setWind(RegattaAndRaceIdentifier raceIdentifier, WindDTO windDTO);
void removeAndUntrackRaces(List<RegattaAndRaceIdentifier> regattaAndRaceIdentifiers);
@@ -274,6 +274,9 @@ public interface SailingServiceWriteAsync extends FileStorageManagementGwtServic
void getCompleteRemoteServerReference(String sailingServerName,
AsyncCallback<RemoteSailingServerReferenceDTO> callback);
void getRemoteEventNamesAndCourseAreas(String baseUrl,
AsyncCallback<Map<String, List<CourseAreaDTO>>> callback);
/**
* Remove mark properties by UUIDs
*
@@ -1458,6 +1458,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
String doYouWantToCreateADefaultRegattaLeaderboard();
String copyCourse();
String copyCourseAreasFromEvent();
String copyCourseAreasFromRemoteServer();
String loadRemoteEvents();
String copyCompetitors();
String smartphoneTracking();
String titelOfChooseNameDialog();
@@ -1485,6 +1485,8 @@ registerBoats=Register Boats
doYouWantToCreateADefaultRegattaLeaderboard=Do you want to create a default Regatta Leaderboard?
copyCourse=Copy Course
copyCourseAreasFromEvent=Copy course areas from event
copyCourseAreasFromRemoteServer=Copy course areas from remote server
loadRemoteEvents=Load
copyCompetitors=Copy Competitors
smartphoneTracking=Smartphone Tracking
titelOfChooseNameDialog=Choose a name
@@ -1455,6 +1455,8 @@ registerBoats=Boote anmelden
doYouWantToCreateADefaultRegattaLeaderboard=Möchten Sie eine Standardrangliste für die Regatta anlegen?
copyCourse=Kurs kopieren
copyCourseAreasFromEvent=Regattabahnen von Veranstaltung kopieren
copyCourseAreasFromRemoteServer=Regattabahnen von externem Server kopieren
loadRemoteEvents=Laden
copyCompetitors=Teilnehmer kopieren
smartphoneTracking=Smartphone Tracking
titelOfChooseNameDialog= Wählen sie einen Namen
@@ -824,6 +824,11 @@ public class SailingServiceWriteImpl extends SailingServiceImpl implements Saili
return createRemoteSailingServerReferenceDTO(serverRef, eventsOrException);
}
@Override
public Map<String, List<CourseAreaDTO>> getRemoteEventNamesAndCourseAreas(final String baseUrl) throws Exception {
return getService().getRemoteEventNamesAndCourseAreas(baseUrl);
}
@Override
public void setRaceIsKnownToStartUpwind(RegattaAndRaceIdentifier raceIdentifier, boolean raceIsKnownToStartUpwind) {
getSecurityService().checkCurrentUserUpdatePermission(raceIdentifier);
@@ -8,10 +8,12 @@ Automatic-Module-Name: com.sap.sailing.server.gateway.interface
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Export-Package: com.sap.sailing.server.gateway.interfaces
Import-Package: com.sap.sailing.domain.common,
com.sap.sailing.domain.common.dto,
com.sap.sse.common,
com.sap.sse.security.util
Require-Bundle: org.json.simple,
com.sap.sailing.domain,
com.sap.sse.shared.android,
org.apache.httpcomponents.httpclient
org.apache.httpcomponents.httpclient,
com.sap.sailing.domain.common
Bundle-ActivationPolicy: lazy
@@ -3,6 +3,8 @@ package com.sap.sailing.server.gateway.interfaces;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@@ -11,6 +13,7 @@ import org.json.simple.parser.ParseException;
import com.sap.sailing.domain.base.RemoteSailingServerReference;
import com.sap.sailing.domain.common.DataImportProgress;
import com.sap.sailing.domain.common.dto.CourseAreaDTO;
import com.sap.sse.security.util.SecuredServer;
import com.sap.sse.shared.json.JsonDeserializationException;
@@ -48,6 +51,8 @@ public interface SailingServer extends SecuredServer {
Iterable<UUID> getEventIds() throws Exception;
Map<String, List<CourseAreaDTO>> getEventNamesAndCourseAreas() throws Exception;
MasterDataImportResult importMasterData(SailingServer from, Iterable<UUID> leaderboardGroupIds, boolean override,
boolean compress, boolean exportWind, boolean exportDeviceConfigs,
boolean exportTrackedRacesAndStartTracking, Optional<UUID> progressTrackingUuid) throws Exception;
@@ -29,6 +29,7 @@ Require-Bundle: com.sap.sailing.domain,
com.sap.sailing.domain.racelogtrackingadapter,
org.eclipse.jetty.osgi-servlet-api;bundle-version="3.1.0",
com.sap.sailing.domain.common,
com.sap.sailing.domain.common.dto,
com.sap.sailing.udpconnector,
com.sap.sailing.server.gateway.serialization,
com.sap.sailing.domain.shared.android,
@@ -27,6 +27,7 @@ import org.json.simple.parser.ParseException;
import com.sap.sailing.domain.base.RemoteSailingServerReference;
import com.sap.sailing.domain.common.DataImportProgress;
import com.sap.sailing.domain.common.dto.CourseAreaDTO;
import com.sap.sailing.domain.common.sharding.ShardingType;
import com.sap.sailing.server.gateway.deserialization.impl.CompareServersResultJsonDeserializer;
import com.sap.sailing.server.gateway.deserialization.impl.DataImportProgressJsonDeserializer;
@@ -42,10 +43,16 @@ import com.sap.sailing.server.gateway.jaxrs.api.LeaderboardsResource;
import com.sap.sailing.server.gateway.jaxrs.api.MasterDataImportResource;
import com.sap.sailing.server.gateway.jaxrs.api.RemoteServerReferenceResource;
import com.sap.sailing.server.gateway.serialization.LeaderboardGroupConstants;
import com.sap.sailing.server.gateway.serialization.impl.CourseAreaJsonSerializer;
import com.sap.sailing.server.gateway.serialization.impl.EventBaseJsonSerializer;
import com.sap.sailing.server.gateway.serialization.impl.VenueJsonSerializer;
import com.sap.sailing.server.gateway.serialization.impl.MasterDataImportResultJsonSerializer;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Position;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.common.impl.MeterDistance;
import com.sap.sailing.server.gateway.deserialization.impl.PositionJsonDeserializer;
import com.sap.sse.security.util.impl.SecuredServerImpl;
import com.sap.sse.shared.json.JsonDeserializationException;
@@ -104,6 +111,38 @@ public class SailingServerImpl extends SecuredServerImpl implements SailingServe
return Util.map(jsonResponse, o->UUID.fromString(((JSONObject) o).get(EventBaseJsonSerializer.FIELD_ID).toString()));
}
// Fetches all public events from the remote server and returns their course areas keyed by event name.
// Reuses the existing /v1/events endpoint which already serializes venue+courseAreas in its response.
@Override
public Map<String, List<CourseAreaDTO>> getEventNamesAndCourseAreas() throws ClientProtocolException, IOException, ParseException {
final URL eventsUrl = new URL(getBaseUrl(), GATEWAY_URL_PREFIX+EventsResource.V1_EVENTS);
final HttpGet getEvents = new HttpGet(eventsUrl.toString());
final JSONArray jsonResponse = (JSONArray) getJsonParsedResponse(getEvents).getA();
final Map<String, List<CourseAreaDTO>> result = new HashMap<>();
for (final Object o : jsonResponse) {
final JSONObject eventJson = (JSONObject) o;
final String eventName = (String) eventJson.get(EventBaseJsonSerializer.FIELD_NAME);
final List<CourseAreaDTO> courseAreas = new ArrayList<>();
final JSONObject venueJson = (JSONObject) eventJson.get(EventBaseJsonSerializer.FIELD_VENUE);
if (venueJson != null) {
final JSONArray courseAreasJson = (JSONArray) venueJson.get(VenueJsonSerializer.FIELD_COURSE_AREAS);
if (courseAreasJson != null) {
for (final Object ca : courseAreasJson) {
final JSONObject caJson = (JSONObject) ca;
final String caName = (String) caJson.get(CourseAreaJsonSerializer.FIELD_NAME);
final JSONObject centerJson = (JSONObject) caJson.get(CourseAreaJsonSerializer.FIELD_CENTER_POSITION);
final Position centerPosition = centerJson != null ? new PositionJsonDeserializer().deserialize(centerJson) : null;
final Number radiusNumber = (Number) caJson.get(CourseAreaJsonSerializer.FIELD_RADIUS_IN_METERS);
final Distance radius = radiusNumber != null ? new MeterDistance(radiusNumber.doubleValue()) : null;
courseAreas.add(new CourseAreaDTO(UUID.randomUUID(), caName, centerPosition, radius));
}
}
}
result.put(eventName, courseAreas);
}
return result;
}
@Override
public MasterDataImportResult importMasterData(SailingServer from, Iterable<UUID> leaderboardGroupIds,
boolean override, boolean compress, boolean exportWind, boolean exportDeviceConfigs,
@@ -58,6 +58,7 @@ import com.sap.sailing.domain.common.CompetitorRegistrationType;
import com.sap.sailing.domain.common.DataImportProgress;
import com.sap.sailing.domain.common.DataImportSubProgress;
import com.sap.sailing.domain.common.DetailType;
import com.sap.sailing.domain.common.dto.CourseAreaDTO;
import com.sap.sailing.domain.common.NoWindException;
import com.sap.sailing.domain.common.RaceFetcher;
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
@@ -767,6 +768,8 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch
Util.Pair<Iterable<EventBase>, Exception> getCompleteRemoteServerReference(RemoteSailingServerReference ref);
Map<String, List<CourseAreaDTO>> getRemoteEventNamesAndCourseAreas(String baseUrl) throws Exception;
/**
* Searches the content of this server, not that of any remote servers referenced by any {@link RemoteSailingServerReference}s.
*/
@@ -61,6 +61,7 @@ import com.sap.sailing.server.security.EventManagerRole;
import com.sap.sailing.server.security.SailingViewerRole;
import com.sap.sailing.server.statistics.TrackedRaceStatisticsCache;
import com.sap.sailing.server.statistics.TrackedRaceStatisticsCacheImpl;
import com.sap.sailing.server.gateway.interfaces.SailingServerFactory;
import com.sap.sailing.shared.server.SharedSailingData;
import com.sap.sse.branding.BrandingConfigurationService;
import com.sap.sse.classloading.ServiceTrackerCustomizerForClassLoaderSupplierRegistrations;
@@ -130,6 +131,8 @@ public class Activator implements BundleActivator {
private FullyInitializedReplicableTracker<SharedSailingData> sharedSailingDataTracker;
private ServiceTracker<ReplicationService, ReplicationService> replicationServiceTracker;
private ServiceTracker<SailingServerFactory, SailingServerFactory> sailingServerFactoryTracker;
public Activator() {
clearPersistentCompetitors = Boolean
@@ -147,6 +150,7 @@ public class Activator implements BundleActivator {
extenderBundleTracker.open();
mailServiceTracker = ServiceTrackerFactory.createAndOpen(context, MailService.class);
replicationServiceTracker = ServiceTrackerFactory.createAndOpen(context, ReplicationService.class);
sailingServerFactoryTracker = ServiceTrackerFactory.createAndOpen(context, SailingServerFactory.class);
sharedSailingDataTracker = FullyInitializedReplicableTracker.createAndOpen(context, SharedSailingData.class);
securityServiceTracker = FullyInitializedReplicableTracker.createAndOpen(context, SecurityService.class);
new Thread(""+this+" initializing RacingEventService in the background") {
@@ -224,6 +228,7 @@ public class Activator implements BundleActivator {
sharedSailingDataTracker.close();
replicationServiceTracker.close();
securityServiceTracker.close();
sailingServerFactoryTracker.close();
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
mbs.unregisterMBean(mBeanName);
}
@@ -296,7 +301,7 @@ public class Activator implements BundleActivator {
/* sensorFixStore */ null, serviceFinderFactory, trackedRegattaListener,
notificationService, trackedRaceStatisticsCache, restoreTrackedRaces, securityServiceTracker,
sharedSailingDataTracker, replicationServiceTracker, scoreCorrectionProviderServiceTracker, competitorProviderServiceTracker,
resultUrlRegistryServiceTracker, brandingConfigurationServiceTracker);
resultUrlRegistryServiceTracker, brandingConfigurationServiceTracker, sailingServerFactoryTracker);
notificationService.setRacingEventService(racingEventService);
// start watching out for MasterDataImportClassLoaderService instances in the OSGi service registry and manage
// the combined class loader accordingly:
@@ -158,6 +158,7 @@ import com.sap.sailing.domain.common.Wind;
import com.sap.sailing.domain.common.WindSource;
import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.common.dto.AnniversaryType;
import com.sap.sailing.domain.common.dto.CourseAreaDTO;
import com.sap.sailing.domain.common.dto.EventType;
import com.sap.sailing.domain.common.dto.FleetDTO;
import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO;
@@ -271,6 +272,8 @@ import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResu
import com.sap.sailing.server.gateway.deserialization.impl.TrackingConnectorInfoJsonDeserializer;
import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer;
import com.sap.sailing.server.gateway.interfaces.MasterDataImportConstants;
import com.sap.sailing.server.gateway.interfaces.SailingServer;
import com.sap.sailing.server.gateway.interfaces.SailingServerFactory;
import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreference;
import com.sap.sailing.server.impl.preferences.model.CompetitorNotificationPreferences;
import com.sap.sailing.server.interfaces.CourseAndMarkConfigurationFactory;
@@ -602,6 +605,8 @@ Replicator {
private final ServiceTracker<CompetitorProvider, CompetitorProvider> competitorProviderServiceTracker;
private ServiceTracker<SailingServerFactory, SailingServerFactory> sailingServerFactoryTracker;
private transient final ConcurrentHashMap<Leaderboard, ScoreCorrectionListener> scoreCorrectionListenersByLeaderboard;
private transient final ConcurrentHashMap<RaceDefinition, RaceTrackingConnectivityParameters> connectivityParametersByRace;
@@ -701,7 +706,8 @@ Replicator {
ServiceTracker<ScoreCorrectionProvider, ScoreCorrectionProvider> scoreCorrectionProviderServiceTracker,
ServiceTracker<CompetitorProvider, CompetitorProvider> competitorProviderServiceTracker,
ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> resultUrlRegistryServiceTracker,
ServiceTracker<BrandingConfigurationService, BrandingConfigurationService> brandingConfigurationServiceTracker) {
ServiceTracker<BrandingConfigurationService, BrandingConfigurationService> brandingConfigurationServiceTracker,
ServiceTracker<SailingServerFactory, SailingServerFactory> sailingServerFactoryTracker) {
this((final RaceLogAndTrackedRaceResolver raceLogResolver) -> {
return new ConstructorParameters() {
private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE
@@ -734,6 +740,7 @@ Replicator {
sailingNotificationService, trackedRaceStatisticsCache, restoreTrackedRaces,
securityServiceTracker, sharedSailingDataTracker, /* replicationServiceTracker */ null,
scoreCorrectionProviderServiceTracker, competitorProviderServiceTracker, resultUrlRegistryServiceTracker);
this.sailingServerFactoryTracker = sailingServerFactoryTracker;
}
private RacingEventServiceImpl(final boolean clearPersistentCompetitorStore, WindStore windStore,
@@ -1837,6 +1844,18 @@ Replicator {
return remoteSailingServerSet.getEventsComplete(ref);
}
@Override
// Creates an anonymous (no auth token) connection to the remote server sufficient for public events.
// SailingServerFactory is wired via OSGi; null check guards against it not yet being available at call time.
public Map<String, List<CourseAreaDTO>> getRemoteEventNamesAndCourseAreas(final String baseUrl) throws Exception {
final SailingServerFactory factory = sailingServerFactoryTracker == null ? null : sailingServerFactoryTracker.getService();
if (factory == null) {
throw new IllegalStateException("SailingServerFactory not available");
}
final SailingServer sailingServer = factory.getSailingServer(new java.net.URL(baseUrl));
return sailingServer.getEventNamesAndCourseAreas();
}
@Override
public void removeRemoteSailingServerReference(String name) {
logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" requested removal of remote sailing server reference "+name);