bug6133: merge main

This commit is contained in:
masha.kashirina
2026-04-25 16:53:34 +02:00
13 changed files with 170 additions and 64 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ FROM eclipse-temurin:8-jdk
# the configuration/github-download-release-assets.sh script to
# obtain the tar.gz file for a specific or the latest "main" release.
# To build with an SAP JVM 8 base image built using Dockerfile_sapjvm, use something like:
#FROM sapjvm8:8.1.108
#FROM sapjvm8:8.1.109
ARG RELEASE
LABEL maintainer=axel.uhl@sap.com
LABEL org.opencontainers.image.description="Sailing Analytics with Java 8"
+1 -1
View File
@@ -1,5 +1,5 @@
FROM buildpack-deps:bullseye
ARG SAPJVM_VERSION=8.1.108
ARG SAPJVM_VERSION=8.1.109
LABEL maintainer=axel.uhl@sap.com
# Download and extract the SAP JVM 8
ENV PATH=${PATH}:/opt/sapjvm_8/bin
+1 -1
View File
@@ -1,4 +1,4 @@
FROM sapmachine:25.0.1
FROM sapmachine:25.0.3
# This Dockerfile assumes that the release to use is provided as
# ${RELEASE}.tar.gz in the current Docker workspace. Use, e.g.,
# the configuration/github-download-release-assets.sh script to
@@ -1,4 +1,4 @@
FROM sapmachine:25.0.1
FROM sapmachine:25.0.3
LABEL maintainer=axel.uhl@sap.com
# Download and extract the release
WORKDIR /home/sailing
@@ -15,6 +15,7 @@ import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import com.sap.sailing.domain.common.orc.ORCCertificate;
import com.sap.sailing.domain.orc.ORCPublicCertificateDatabase.CertificateFamily;
import com.sap.sailing.domain.orc.ORCPublicCertificateDatabase.CertificateHandle;
import com.sap.sailing.domain.orc.ORCPublicCertificateDatabase.CountryOverview;
import com.sap.sailing.domain.orc.impl.ORCPublicCertificateDatabaseImpl;
@@ -52,8 +53,9 @@ public class FailIfNoValidOrcCertificateRule implements BeforeTestExecutionCallb
countryWithMostValidCertificates = StreamSupport
.stream(db.getCountriesWithValidCertificates().spliterator(), /* parallel */ false)
.max((c1, c2) -> c1.getCertCount() - c2.getCertCount()).get();
Iterable<CertificateHandle> certificateHandles = db.search(countryWithMostValidCertificates.getIssuingCountry(),
countryWithMostValidCertificates.getVPPYear(), null, null, null, null, /* includeInvalid */ false);
Iterable<CertificateHandle> certificateHandles = Util.filter(db.search(countryWithMostValidCertificates.getIssuingCountry(),
countryWithMostValidCertificates.getVPPYear(), null, null, null, null, /* includeInvalid */ false),
certHandle->certHandle.getFamily() != CertificateFamily.ORC_LIGHT); // exclude LITE certificates
final List<CertificateHandle> randomSubset = new ArrayList<>();
Util.addAll(certificateHandles, randomSubset);
Collections.shuffle(randomSubset);
@@ -2,9 +2,11 @@ package com.sap.sailing.domain.orc;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.IOException;
import java.util.Collection;
import java.util.logging.Logger;
import org.json.simple.parser.ParseException;
import org.junit.jupiter.api.Test;
@@ -13,6 +15,8 @@ import com.sap.sailing.domain.common.orc.ORCCertificate;
public class TestORCCertificateImporterJSON extends AbstractORCCertificateImporterTest {
private static final Logger logger = Logger.getLogger(TestORCCertificateImporterJSON.class.getName());
@Test
public void testSimpleLocalJSONFileRead() throws IOException, ParseException {
testSimpleLocalFileRead("GER2019.json", "GER20041179");
@@ -27,9 +31,19 @@ public class TestORCCertificateImporterJSON extends AbstractORCCertificateImport
@Test
public void testSimpleOnlineFileRead() throws IOException, ParseException, InterruptedException {
Collection<ORCCertificate> certificates = FailIfNoValidOrcCertificateRule.getAvailableCerts();
final ORCCertificate referenceCert = certificates.stream().findFirst().get();
assertNotNull(referenceCert);
assertTrue(referenceCert.getWindwardLeewardSpeedPrediction().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
assertTrue(referenceCert.getLongDistanceSpeedPredictions().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
for (final ORCCertificate referenceCert : certificates) {
assertNotNull(referenceCert);
// some certificates are not fully filled with allowances for all types of PCS pre-sets; we need to check whether
// the certificate at hand has those we need for this test; else, we keep going and use another one.
// We're already excluding LITE certificates to reduce chances for this case
if (referenceCert.getLongDistanceSpeedPredictions().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]) != null) {
assertTrue(referenceCert.getWindwardLeewardSpeedPrediction().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
assertTrue(referenceCert.getLongDistanceSpeedPredictions().get(ORCCertificate.ALLOWANCES_TRUE_WIND_SPEEDS[0]).getDuration(ORCCertificate.NAUTICAL_MILE).asSeconds() > 10);
return;
} else {
logger.info("No valid GPH found in certificate "+referenceCert.getBoatName()+" with sail number "+referenceCert.getSailNumber());
}
}
fail("Found only certificates with no valid long distance speed predictions; this seems unlikely and is probably an error");
}
}
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.time.Instant;
import java.time.LocalDate;
@@ -153,51 +154,59 @@ public class TestORCPublicCertificateDatabase {
@Test
public void testGetCertificate() throws Exception {
Collection<ORCCertificate> certificates = FailIfNoValidOrcCertificateRule.getAvailableCerts();
final ORCCertificate cert = certificates.stream().findFirst().get();
Iterable<CertificateHandle> certHandles = db.search(/* country */ null, LocalDate.now().getYear(), /* referenceNumber */ null, cert.getBoatName(),
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
if (Util.isEmpty(certHandles)) {
// there were certs; get one from the previous year
certHandles = db.search(null, LocalDate.now().getYear()-1, null, cert.getBoatName(),
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
// some certificates seem to be lacking a GPH value; can't use those for this test; we're explicitly excluding LITE certificates already...
for (final ORCCertificate cert : certificates) {
if (cert.getGPH() != null) {
Iterable<CertificateHandle> certHandles = db.search(/* country */ null, LocalDate.now().getYear(), /* referenceNumber */ null, cert.getBoatName(),
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
if (Util.isEmpty(certHandles)) {
// there were certs; get one from the previous year
certHandles = db.search(null, LocalDate.now().getYear()-1, null, cert.getBoatName(),
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
}
if (Util.isEmpty(certHandles)) {
// there were certs; try searching by reference number
certHandles = db.search(null, LocalDate.now().getYear(), cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
}
if (Util.isEmpty(certHandles)) {
// still nothing? Then try by reference number in previous year:
certHandles = db.search(null, LocalDate.now().getYear()-1, cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
}
Optional<CertificateHandle> certificateHandle = Optional.ofNullable(certHandles.iterator().hasNext() ? certHandles.iterator().next() : null);
assertTrue(certificateHandle.isPresent(), "No certificate found for handle "+certificateHandle+
" extracted from certificates "+certificates);
final String referenceNumber = certificateHandle.get().getReferenceNumber();
final CertificateHandle handle = db.getCertificateHandle(referenceNumber);
final ORCCertificate result = db.getCertificate(referenceNumber, handle.getFamily());
assertNotNull(result, "Unable to load certificate for reference number "+referenceNumber+" from handle "+certificateHandle);
assertEquals(handle.getGPH(), result.getGPH().asSeconds(), 0.00001);
// Use some tolerance as we found differences as much as 5s between the dxtDate in the handle coming from the XML search result
// and the IssueDate field in the JSON. Both suggest to report millisecond accuracy, but dxtDate always seems to have the
// milliseconds as "000" explaining many sub-second differences. But in some cases differences were significantly bigger.
assertEquals(handle.getIssueDate().asMillis(), result.getIssueDate().asMillis(), 10000.0, "Issue dates of certificate with reference number "+referenceNumber+
" varies between current year result handle ("+handle.getIssueDate()+") and certificate ("+
result.getIssueDate()+").");
assertEquals(handle.getSailNumber(), result.getSailNumber());
return;
} else {
logger.info("No valid GPH found in certificate "+cert.getBoatName()+" with sail number "+cert.getSailNumber());
}
}
if (Util.isEmpty(certHandles)) {
// there were certs; try searching by reference number
certHandles = db.search(null, LocalDate.now().getYear(), cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
}
if (Util.isEmpty(certHandles)) {
// still nothing? Then try by reference number in previous year:
certHandles = db.search(null, LocalDate.now().getYear()-1, cert.getReferenceNumber(), /* boat name may have deviated due to special characters */ null,
cert.getSailNumber(), /*
* boat class name; could be set to cert.getBoatClassName() but there are
* deviations in ORC DBs and query API, so leaving null:
*/ null, /* includeInvalid */ false);
}
Optional<CertificateHandle> certificateHandle = Optional.ofNullable(certHandles.iterator().hasNext() ? certHandles.iterator().next() : null);
assertTrue(certificateHandle.isPresent(), "No certificate found for handle "+certificateHandle+
" extracted from certificates "+certificates);
final String referenceNumber = certificateHandle.get().getReferenceNumber();
final CertificateHandle handle = db.getCertificateHandle(referenceNumber);
final ORCCertificate result = db.getCertificate(referenceNumber, handle.getFamily());
assertNotNull(result, "Unable to load certificate for reference number "+referenceNumber+" from handle "+certificateHandle);
assertEquals(handle.getGPH(), result.getGPH().asSeconds(), 0.00001);
// Use some tolerance as we found differences as much as 5s between the dxtDate in the handle coming from the XML search result
// and the IssueDate field in the JSON. Both suggest to report millisecond accuracy, but dxtDate always seems to have the
// milliseconds as "000" explaining many sub-second differences. But in some cases differences were significantly bigger.
assertEquals(handle.getIssueDate().asMillis(), result.getIssueDate().asMillis(), 10000.0, "Issue dates of certificate with reference number "+referenceNumber+
" varies between current year result handle ("+handle.getIssueDate()+") and certificate ("+
result.getIssueDate()+").");
assertEquals(handle.getSailNumber(), result.getSailNumber());
fail("No certificate found with a valid GPH; that seems very suspicious and lets this test case fail.");
}
@FailIfNoValidOrcCertificates
@@ -72,8 +72,8 @@ public class SmartFutureCacheTest {
sfc.triggerUpdate("humba", /* update interval */ null);
try {
// during the first call, expecting exception
sfc.get("humba", /* waitForLatest */ true);
fail("Expected RuntimeException because computeCacheUpdate threw one");
final String result = sfc.get("humba", /* waitForLatest */ true);
fail("Expected RuntimeException because computeCacheUpdate threw one; instead, it returned "+result);
} catch (RuntimeException expected) {
assertSame(ExecutionException.class, expected.getCause().getClass());
}
@@ -105,7 +105,7 @@ public interface ORCPublicCertificateDatabase {
ORCPublicCertificateDatabase INSTANCE = new ORCPublicCertificateDatabaseImpl();
public enum CertificateFamily {
UNKNOWN(0, ""), ORC(1, "ORC"), SUPER_YACHT(2, "SY"), DOUBLE_HANDED(3, "DH"), MULTI_HULL(4, "Mu");
UNKNOWN(0, ""), ORC(1, "ORC"), SUPER_YACHT(2, "SY"), DOUBLE_HANDED(3, "DH"), MULTI_HULL(4, "Mu"), NON_SPINNAKER(5, "NS"), ORC_LIGHT(6, "LITE");
private final int familyId;
private final String familyQueryParamValue;
@@ -137,8 +137,8 @@ public interface ORCPublicCertificateDatabase {
}
/**
* Data about valid certificates in a country, as obtained, e.g., from http://data.orc.org/public/WPub.dll/RMS. Such a
* record, in its original XML representation, looks like this:
* Data about valid certificates in a country, as obtained, e.g., from http://data.orc.org/public/WPub.dll/RMS. Such
* a record, in its original XML representation, looks like this:
*
* <pre>
&lt;CountryId&gt;AUS&lt;/CountryId&gt;
@@ -152,6 +152,87 @@ public interface ORCPublicCertificateDatabase {
&lt;RMSCode&gt;CLUB&lt;/RMSCode&gt;
* </pre>
*
* Mappings of family and type work as follows:
*
* Used for parsing the ORC public API (WPub.dll) RMS/JSON outputs. Note: Family 6 (ORC Light) certificates
* generally lack GPH and Performance Curve data required for PCS calculations.
*
* <table border="1">
* <caption>ORC Family and Certificate Type Codes</caption>
* <tr>
* <th>Family ID</th>
* <th>Family Name</th>
* <th>certType</th>
* <th>Certificate Name</th>
* </tr>
* <tr>
* <td>1</td>
* <td>ORC Standard</td>
* <td>2</td>
* <td>International (ORCi)</td>
* </tr>
* <tr>
* <td>1</td>
* <td>ORC Standard</td>
* <td>3</td>
* <td>Club</td>
* </tr>
* <tr>
* <td>2</td>
* <td>Super Yacht</td>
* <td>7</td>
* <td>ORCsy</td>
* </tr>
* <tr>
* <td>3</td>
* <td>Double Handed</td>
* <td>8</td>
* <td>DH International</td>
* </tr>
* <tr>
* <td>3</td>
* <td>Double Handed</td>
* <td>9</td>
* <td>DH Club</td>
* </tr>
* <tr>
* <td>4</td>
* <td>Multihull</td>
* <td>15</td>
* <td>Multihull International</td>
* </tr>
* <tr>
* <td>4</td>
* <td>Multihull</td>
* <td>16</td>
* <td>Multihull Club</td>
* </tr>
* <tr>
* <td>5</td>
* <td>Non Spinnaker</td>
* <td>10</td>
* <td>NS International</td>
* </tr>
* <tr>
* <td>5</td>
* <td>Non Spinnaker</td>
* <td>11</td>
* <td>NS Club</td>
* </tr>
* <tr>
* <td>6</td>
* <td>ORC Light</td>
* <td>13</td>
* <td>Light (Standard)</td>
* </tr>
* <tr>
* <td>6</td>
* <td>ORC Light</td>
* <td>14</td>
* <td>Light (Double Handed)</td>
* </tr>
* </table>
*
* @author Axel Uhl (D043530)
*/
public interface CountryOverview {
@@ -13,6 +13,7 @@ public class FavoritesResult implements Result {
public FavoritesResult(FavoriteBoatClassesDTO favoriteBoatClasses, FavoriteCompetitorsDTO favoriteCompetitors, boolean isSubscribedToFeatureAndCommunityUpdates) {
this.favoriteBoatClasses = favoriteBoatClasses;
this.favoriteCompetitors = favoriteCompetitors;
this.isSubscribedToFeatureAndCommunityUpdates = isSubscribedToFeatureAndCommunityUpdates;
}
public FavoriteBoatClassesDTO getFavoriteBoatClasses() {
@@ -26,5 +27,4 @@ public class FavoritesResult implements Result {
public boolean getIsSubscribedToFeatureAndCommunityUpdates() {
return isSubscribedToFeatureAndCommunityUpdates;
}
}
@@ -26,7 +26,7 @@ public class GetFavoritesAction implements SailingAction<FavoritesResult> {
@GwtIncompatible
public FavoritesResult execute(SailingDispatchContext ctx) throws DispatchException {
return new FavoritesResult(getFavoriteBoatClasses(ctx), getFavoriteCompetitors(ctx),
getDidOptOutOfFeatureAndCommunityEmails(ctx));
!getDidOptOutOfFeatureAndCommunityEmails(ctx));
}
@GwtIncompatible
@@ -31,9 +31,9 @@
Data Mining as a dimension for filtering and grouping.</li>
<ii>When updates to user accounts are carried out using the API, not passing a property (such as
company affiliation or preferred locale) will no longer reset that value to <tt>null</tt>
but instead leave it unchanged.</ii>
but instead leave it unchanged.</li>
<li>A new "Eraser" action has been added to the leaderboards table at the top of the
Connectors / Smartphone Tracking panel. If the leadeboard is a "Regatta" leaderboard
Connectors / Smartphone Tracking panel. If the leaderboard is a "Regatta" leaderboard
and the corresponding Regatta is configured to control tracking times from race
start/finish times, using the eraser action will remove any explicit start/end of tracking
time specifications of the race logs of all races in the leaderboard that have valid
+1 -1
View File
@@ -29,7 +29,7 @@ Displays designs displaying the login pages for the management-console.
The View is already implemented. Overall Authorization and related event propagation is missing.
![Image not found](https://static.sapsailing.com/management-console/login/Login%20-%20Mobile.png)
See https://static.sapsailing.com/management-console/login for a full list of designs.
See [https://static.sapsailing.com/management-console/login](https://static.sapsailing.com/management-console/login) for a full list of designs.
### Event