bug6319: remove dead includeInvalid search parameter; export ORC_API_KEY

The key-based sas.orc.org API routes all active-certificate search through the
activecerts endpoint, which returns active certificates only. The includeInvalid
flag on ORCPublicCertificateDatabase.search(...) could therefore never change the
result and was dead. Remove it from the interface and impl, the default
getCertificateHandle, the two fuzzy-search call sites, SailingServiceImpl, and all
ORC test call sites. The sole route to an inactive certificate in this adapter is to
download it by reference number via DownBoatRMS_byrefno (getCertificate /
getCertificateHandle); the search Javadoc now states this explicitly.

Also export ORC_API_KEY from java/target/env-default-rules.sh so the Java VM can read
it via System.getenv(...). configuration/secrets assigns it as a plain (non-exported)
shell variable that the java child process would not otherwise inherit; it is passed
as an environment variable rather than a -D system property so it is not exposed on
the process command line.

Assisted-By: Claude Opus 4.8 (claude-opus-5-5)
This commit is contained in:
Axel Uhl committed 2026-10-17 21:27:46 +02:00
1 parent fd97a6b8a2
commit 85ccfc4e5e
9 files changed
+287 -113

No files matched your search

@@ -57,8 +57,7 @@ public class FailIfNoValidOrcCertificateRule implements BeforeTestExecutionCallb
.max((c1, c2) -> c1.getCertCount() - c2.getCertCount()).get();
final Iterable<CertificateHandle> certificateHandles = Util.filter(
db.search(countryWithMostValidCertificates.getIssuingCountry(),
countryWithMostValidCertificates.getVPPYear(), null, null, null, null,
/* includeInvalid */ false),
countryWithMostValidCertificates.getVPPYear(), null, null, null, null),
certHandle -> certHandle.getFamily() != CertificateFamily.ORC_LIGHT); // exclude LITE certificates
final List<CertificateHandle> randomSubset = new ArrayList<>();
Util.addAll(certificateHandles, randomSubset);
@@ -0,0 +1,127 @@
package com.sap.sailing.domain.orc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.impl.ORCPublicCertificateDatabaseImpl;
import com.sap.sse.common.Util;
/**
* Active-certificate search coverage against the key-based {@code activecerts} endpoint of the ORC public certificate
* API. The {@code activecerts} endpoint only ever serves <em>active</em> certificates, so instead of pinning a fixed
* reference number (which would expire and need replacing every year) these tests pick a live, currently active
* certificate as a "seed" and then run the search variants against that seed's own values. This keeps the tests
* self-maintaining as the ORC database evolves.
* <p>
* These tests are deliberately isolated in their own class so that the (comparatively large) migration to the key-based
* API can be reviewed without this live-fixture coverage complicating the diff of the pre-existing tests in
* {@link TestORCPublicCertificateDatabase}, whose reference-number download / update coverage keeps using the
* inactive SOULMATE certificate reachable only through the {@code DownBoatRMS_byrefno} endpoint.
*/
@ExtendWith(FailIfNoValidOrcCertificateRule.class)
public class TestORCActiveCertificateSearch {
private ORCPublicCertificateDatabase db;
@BeforeEach
public void setUp() {
db = new ORCPublicCertificateDatabaseImpl();
}
/**
* Picks a live, currently active certificate handle to use as the seed for the search tests. It searches the
* current VPP year for the country of one of the certificates the {@link FailIfNoValidOrcCertificateRule} probed,
* so the seed is guaranteed to be an active certificate that the {@code activecerts} endpoint can find again.
*/
private CertificateHandle pickSeedHandle() throws Exception {
final List<CertificateHandle> seeds = new ArrayList<>();
for (final ORCCertificate cert : FailIfNoValidOrcCertificateRule.getAvailableCerts()) {
for (final int year : new int[] { LocalDate.now().getYear(), LocalDate.now().getYear() - 1 }) {
if (seeds.isEmpty()) {
final Iterable<CertificateHandle> handles = db.search(cert.getIssuingCountry(), year,
/* referenceNumber */ null, /* yachtName */ null, /* sailNumber */ null,
/* boatClassName */ null);
for (final CertificateHandle handle : handles) {
if (seeds.isEmpty() && handle.getFamily() != CertificateFamily.ORC_LIGHT) {
seeds.add(handle);
}
}
}
}
}
assertTrue(!seeds.isEmpty(), "Could not find any active certificate to seed the search tests from.");
return seeds.get(0);
}
@FailIfNoValidOrcCertificates
@Test
public void testSimpleSearchByRefNo() throws Exception {
final CertificateHandle seed = pickSeedHandle();
final Iterable<CertificateHandle> handles = db.search(/* country */ null, /* year */ null,
seed.getReferenceNumber(), /* yachtName */ null, /* sailNumber */ null, /* boatClassName */ null);
boolean found = false;
for (final CertificateHandle handle : handles) {
found = found || seed.getReferenceNumber().equals(handle.getReferenceNumber());
}
assertTrue(found, "No active certificate found for seed reference number " + seed.getReferenceNumber());
}
@FailIfNoValidOrcCertificates
@Test
public void testSearchByYachtNameAndSailNumberAndYear() throws Exception {
final CertificateHandle seed = pickSeedHandle();
final int seedYear = Instant.ofEpochMilli(seed.getIssueDate().asMillis()).atOffset(ZoneOffset.UTC)
.toLocalDate().getYear();
final Iterable<CertificateHandle> handles = db.search(/* country */ null, seedYear, /* referenceNumber */ null,
seed.getYachtName(), seed.getSailNumber(), /* boatClassName */ null);
boolean found = false;
for (final CertificateHandle handle : handles) {
found = found || seed.getReferenceNumber().equals(handle.getReferenceNumber());
}
assertTrue(found, "Expected to find reference number " + seed.getReferenceNumber()
+ " when searching by its own yacht name " + seed.getYachtName() + ", sail number "
+ seed.getSailNumber() + " and year " + seedYear);
}
@FailIfNoValidOrcCertificates
@Test
public void testSearchByYachtName() throws Exception {
final CertificateHandle seed = pickSeedHandle();
final Iterable<CertificateHandle> handles = db.search(/* country */ null, /* year */ null,
/* referenceNumber */ null, seed.getYachtName(), /* sailNumber */ null, /* boatClassName */ null);
assertTrue(!Util.isEmpty(handles), "Expected at least one active certificate for yacht name "
+ seed.getYachtName());
boolean found = false;
for (final CertificateHandle handle : handles) {
found = found || seed.getReferenceNumber().equals(handle.getReferenceNumber());
}
assertTrue(found, "Expected the seed reference number " + seed.getReferenceNumber()
+ " among the results for yacht name " + seed.getYachtName());
}
@FailIfNoValidOrcCertificates
@Test
public void testCertificateHandleAPI() throws Exception {
final CertificateHandle seed = pickSeedHandle();
final CertificateHandle handle = db.getCertificateHandle(seed.getReferenceNumber());
assertNotNull(handle, "No certificate handle for reference number " + seed.getReferenceNumber());
assertEquals(seed.getReferenceNumber(), handle.getReferenceNumber());
assertNotNull(handle.getIssueDate(), "Certificate handle for " + seed.getReferenceNumber()
+ " carries no issue date");
assertTrue(handle.getIssueDate().asMillis() > 0, "Certificate handle for " + seed.getReferenceNumber()
+ " carries a non-positive issue date");
}
}
@@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -31,7 +30,10 @@ import com.sap.sailing.domain.common.orc.impl.ORCPerformanceCurveCourseImpl;
import com.sap.sailing.domain.common.orc.impl.ORCPerformanceCurveLegImpl;
import com.sap.sailing.domain.orc.impl.ORCCertificatesJsonImporter;
import com.sap.sailing.domain.orc.impl.ORCPerformanceCurveImpl;
import com.sap.sailing.domain.orc.impl.ORCPublicCertificateDatabaseImpl;
import com.sap.sse.common.Bearing;
import com.sap.sse.common.CountryCode;
import com.sap.sse.common.CountryCodeFactory;
import com.sap.sse.common.Distance;
import com.sap.sse.common.Duration;
import com.sap.sse.common.Speed;
@@ -55,8 +57,6 @@ public class TestORCPerformanceCurve {
private static ORCCertificatesCollection importerOnline;
private static final String RESOURCES = "resources/orc/";
private static final String API_KEY_ENV_VARIABLE_NAME = "ORC_API_KEY";
private static final String API_BASE_URL = "https://sas.orc.org/api/";
@BeforeAll
public static void initialize() throws IOException, ParseException {
@@ -74,15 +74,15 @@ public class TestORCPerformanceCurve {
// Local File new format with specific TWA/TWS bins:
final File fileWithSpecificBins = new File(RESOURCES + "newFormatCertificate.json");
importerWithSpecificBins = new ORCCertificatesJsonImporter().read(new FileInputStream(fileWithSpecificBins));
// Online File: a bulk GER download via the key-based sas.orc.org API. The key is read from the ORC_API_KEY
// environment variable (the same variable the adapter uses); it must be present, so a missing key FAILS the
// test setup rather than silently skipping. The key is never logged.
final String apiKey = System.getenv(API_KEY_ENV_VARIABLE_NAME);
assertNotNull(apiKey, "The " + API_KEY_ENV_VARIABLE_NAME
+ " environment variable must be set for the online ORC certificate import test.");
final String bulkDownloadUrl = API_BASE_URL + apiKey + "/DownRMS?CountryId=GER&ext=json";
// Online File: a bulk GER download via the key-based sas.orc.org API. All endpoint and key handling lives in
// the adapter; the test only asks it for the bulk certificate stream so there is no duplicated URL construction
// or env-var/key logic here. The adapter reads the ORC_API_KEY environment variable and throws when it is
// absent, so a missing key FAILS this setup rather than silently skipping, and the key is never logged.
final CountryCode ger = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName("GER");
logger.info("Trying to read bulk GER certificates from the key-based ORC API");
importerOnline = new ORCCertificatesJsonImporter().read(new URL(bulkDownloadUrl).openStream());
importerOnline = new ORCCertificatesJsonImporter().read(
((ORCPublicCertificateDatabaseImpl) ORCPublicCertificateDatabase.INSTANCE)
.openBulkCertificatesJsonStream(ger));
}
@Test
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import com.sap.sailing.domain.base.impl.BoatClassImpl;
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.impl.ORCPublicCertificateDatabaseImpl;
import com.sap.sse.common.Util;
@@ -41,12 +42,21 @@ import com.sap.sse.common.Util;
@ExtendWith(FailIfNoValidOrcCertificateRule.class)
public class TestORCPublicCertificateDatabase {
private static final Logger logger = Logger.getLogger(TestORCPublicCertificateDatabase.class.getName());
// A known reference number for the boat SOULMATE. The certificate is no longer active, so it can only be reached
// through the DownBoatRMS_byrefno endpoint (getCertificate / searchForUpdate), not through the active-certificate
// search. The former active-certificate search tests that used to key on this boat now live in
// TestORCActiveCertificateSearch against a currently active certificate.
private static final String SOULMATE_REF_NO = "FRA00013881";
private static final String SOULMATE_YACHT_NAME = "SOULMATE";
private static final String SOULMATE_BOAT_CLASS = "MOSQUITO 85";
private static final String SOULMATE_SAIL_NUMBER = "DEN- 13";
private static final double SOULMATE_GPH_SECONDS = 662.4;
private ORCPublicCertificateDatabase db;
private Map<String, Date> dateComparisonMap = new LinkedHashMap<String, Date>();
private List<String> dateFailureCases = Arrays.asList("2019-02-21T10:44GMT+2","2019-02-21T10:38+0800","2019-02-21T10:38+08:00",
"2019-02-21T10:38-08","2019-02-21T10:38Z","2019-02-21T10z","2019-02-21T10:38z");
@BeforeEach
public void setUp() {
db = new ORCPublicCertificateDatabaseImpl();
@@ -57,6 +67,7 @@ public class TestORCPublicCertificateDatabase {
dateComparisonMap.put("2019-02-21T10:38:59Z", Date.from(ZonedDateTime.parse("2019-02-21T10:38:59Z").toInstant()));
// dateComparisonMap.put("2019-02-21T12:00:00.000GMT+2", Date.from(ZonedDateTime.of(2019, 2, 21, 10, 38, 59, 0, ZoneId.systemDefault()).toInstant()));
// // dateComparisonMap.put("2019-02-21T15:00:00.000 GMT-2", Date.from(ZonedDateTime.of(2019, 2, 21, 10, 38, 59, 0, ZoneId.systemDefault()).toInstant()));
dateComparisonMap.put("2019-02-21T10:38:59Z", Date.from(ZonedDateTime.parse("2019-02-21T10:38:59Z").toInstant()));
dateComparisonMap.put("2019-02-21T10:38:55.000-0800",
Date.from(ZonedDateTime.parse("2019-02-21T10:38:55.000-0800", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")).toInstant())
);
@@ -90,83 +101,28 @@ public class TestORCPublicCertificateDatabase {
);
}
/**
* Picks a live reference number from the certificates probed by {@link FailIfNoValidOrcCertificateRule} and returns
* the corresponding {@link CertificateHandle}. Fails the test when no seed handle can be obtained, which would
* indicate a broken search rather than a legitimate empty result.
*/
private CertificateHandle pickSeedHandle() throws Exception {
final Collection<ORCCertificate> probed = FailIfNoValidOrcCertificateRule.getAvailableCerts();
assertFalse(probed.isEmpty(), "No ORC certificates could be probed; cannot seed the search tests.");
final ORCCertificate seedCertificate = probed.iterator().next();
final CertificateHandle handle = db.getCertificateHandle(seedCertificate.getReferenceNumber());
assertNotNull(handle, "Unable to obtain a handle for seed reference number " + seedCertificate.getReferenceNumber());
return handle;
}
@FailIfNoValidOrcCertificates
@Test
public void testSimpleSearchByRefNo() throws Exception {
final String referenceNumber = pickSeedHandle().getReferenceNumber();
final Iterable<CertificateHandle> result = db.search(/* country */ null, /* yearOfIssuance */ null,
/* referenceNumber */ referenceNumber, /* yachtName */ null, /* sailNumber */ null,
/* boatClassName */ null, /* includeInvalid */ true);
assertEquals(1, Util.size(result));
assertEquals(referenceNumber, result.iterator().next().getReferenceNumber());
final ORCCertificate certificate = db.getCertificate(SOULMATE_REF_NO, CertificateFamily.ORC);
assertSoulmate(SOULMATE_REF_NO, certificate);
}
@FailIfNoValidOrcCertificates
@Test
public void testSearchByYachtNameAndSailNumberAndYear() throws Exception {
final CertificateHandle seed = pickSeedHandle();
final Iterable<CertificateHandle> result = db.search(/* country */ null,
/* yearOfIssuance */ LocalDate.now().getYear(), /* referenceNumber */ null,
/* yachtName */ seed.getYachtName(), /* sailNumber */ seed.getSailNumber(), /* boatClassName */ null,
/* includeInvalid */ true);
boolean found = false;
for (final CertificateHandle handle : result) {
if (handle.getReferenceNumber().equals(seed.getReferenceNumber())) {
found = true;
}
}
assertTrue(found, "Search by yacht name and sail number did not return the seed certificate " + seed.getReferenceNumber());
}
@FailIfNoValidOrcCertificates
@Test
public void testSearchByYachtName() throws Exception {
final CertificateHandle seed = pickSeedHandle();
final Iterable<CertificateHandle> result = db.search(/* country */ null, /* yearOfIssuance */ null,
/* referenceNumber */ null, /* yachtName */ seed.getYachtName(), /* sailNumber */ null,
/* boatClassName */ null, /* includeInvalid */ true);
boolean found = false;
for (final CertificateHandle handle : result) {
if (handle.getReferenceNumber().equals(seed.getReferenceNumber())) {
found = true;
}
}
assertTrue(found, "Search by yacht name did not return the seed certificate " + seed.getReferenceNumber());
}
@FailIfNoValidOrcCertificates
@Test
public void testCertificateHandleAPI() throws Exception {
final String referenceNumber = pickSeedHandle().getReferenceNumber();
final CertificateHandle result = db.getCertificateHandle(referenceNumber);
assertEquals(referenceNumber, result.getReferenceNumber());
assertNotNull(result.getIssueDate());
final CertificateHandle handle = db.getCertificateHandle(SOULMATE_REF_NO);
assertSoulmate(SOULMATE_REF_NO, handle);
}
@FailIfNoValidOrcCertificates
@Test
public void testCertificateUpdate() throws Exception {
final CertificateHandle oldHandle = pickSeedHandle();
final ORCCertificate result = db.searchForUpdate(oldHandle);
final ORCCertificate oldCertificate = db.getCertificate(SOULMATE_REF_NO, CertificateFamily.ORC);
assertSoulmate(SOULMATE_REF_NO, oldCertificate);
final ORCCertificate result = db.searchForUpdate(oldCertificate);
if (result != null) {
assertEquals(oldHandle.getIssuingCountry(), result.getIssuingCountry());
assertEquals(oldHandle.getReferenceNumber(), result.getReferenceNumber());
assertEquals(oldCertificate.getIssuingCountry(), result.getIssuingCountry());
assertFalse(oldCertificate.getIssueDate().after(result.getIssueDate()));
} else {
logger.warning("Couldn't find an update to certificate with reference number "+oldHandle.getReferenceNumber()+
logger.warning("Couldn't find an update to certificate with reference number "+SOULMATE_REF_NO+
" anymore. Consider updating this test case to using a different certificate that still has a valid update.");
}
}
@@ -178,23 +134,52 @@ public class TestORCPublicCertificateDatabase {
// 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) {
final Iterable<CertificateHandle> certHandles = db.search(/* country */ cert.getIssuingCountry(),
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);
final Optional<CertificateHandle> certificateHandle = Optional
.ofNullable(certHandles.iterator().hasNext() ? certHandles.iterator().next() : 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);
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);
}
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);
}
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);
}
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);
// The activecerts search result handle carries no GPH; the GPH is only populated on the full certificate
// download, so assert against the downloaded certificate's GPH instead.
// The active-certificate search result handle carries no GPH; the GPH is only populated on the full
// certificate download, so assert that the downloaded certificate carries a GPH rather than comparing
// it to the handle's (absent) GPH.
assertNotNull(result.getGPH(), "No GPH on downloaded certificate "+referenceNumber);
// Use some tolerance as we found differences as much as 5s between the dxtDate in the handle coming from the 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 {
@@ -203,7 +188,7 @@ public class TestORCPublicCertificateDatabase {
}
fail("No certificate found with a valid GPH; that seems very suspicious and lets this test case fail.");
}
@FailIfNoValidOrcCertificates
@Test
public void testParallelFuzzySearch() throws InterruptedException, ExecutionException {
@@ -219,7 +204,7 @@ public class TestORCPublicCertificateDatabase {
}
assertTrue(isYearFound);
}
@Test
public void testParseDateSuccessCases() {
for (String date : dateComparisonMap.keySet()) {
@@ -227,7 +212,7 @@ public class TestORCPublicCertificateDatabase {
assertEquals(convertedDate, dateComparisonMap.get(date));
}
}
@Test
public void testShould() throws Exception {
assertThrows(DateTimeParseException.class, ()->{
@@ -238,6 +223,26 @@ public class TestORCPublicCertificateDatabase {
});
}
private void assertSoulmate(final String referenceNumber, final ORCCertificate certificate) {
assertNotNull(certificate, "Unable to load certificate with reference number " + referenceNumber);
assertEquals(referenceNumber, certificate.getReferenceNumber());
assertEquals(SOULMATE_YACHT_NAME, certificate.getBoatName());
assertEquals(SOULMATE_BOAT_CLASS, certificate.getBoatClassName());
assertEquals(SOULMATE_SAIL_NUMBER, certificate.getSailNumber());
assertEquals("FRA", certificate.getIssuingCountry().getThreeLetterIOCCode());
assertEquals(SOULMATE_GPH_SECONDS, certificate.getGPH().asSeconds(), 0.05);
}
private void assertSoulmate(final String referenceNumber, final CertificateHandle handle) {
assertNotNull(handle, "Unable to load certificate handle with reference number " + referenceNumber);
assertEquals(referenceNumber, handle.getReferenceNumber());
assertEquals(SOULMATE_YACHT_NAME, handle.getYachtName());
assertEquals(SOULMATE_BOAT_CLASS, handle.getBoatClassName());
assertEquals(SOULMATE_SAIL_NUMBER, handle.getSailNumber());
assertEquals("FRA", handle.getIssuingCountry().getThreeLetterIOCCode());
assertEquals(SOULMATE_GPH_SECONDS, handle.getGPH(), 0.05);
}
private boolean assertFoundYear(final Set<ORCCertificate> certificates, int year) {
return certificates.stream().map(cert -> {
LocalDate certDate = Instant.ofEpochMilli(cert.getIssueDate().asMillis()).atOffset(ZoneOffset.UTC)
@@ -24,7 +24,7 @@ Export-Package: com.sap.sailing.domain.base,
com.sap.sailing.domain.markpassinghash.impl;x-friends:="com.sap.sailing.domain.persistence",
com.sap.sailing.domain.masterdataimport,
com.sap.sailing.domain.orc,
com.sap.sailing.domain.orc.impl;x-friends:="com.sap.sailing.domain.test,com.sap.sailing.domain.test,com.sap.sailing.mongodb.test",
com.sap.sailing.domain.orc.impl;x-friends:="com.sap.sailing.domain.test,com.sap.sailing.mongodb.test",
com.sap.sailing.domain.polars,
com.sap.sailing.domain.racelog,
com.sap.sailing.domain.racelog.impl,
@@ -22,10 +22,9 @@ import com.sap.sse.common.Util;
* (see {@code https://sas.orc.org/api-docs.php}). It can search for certificates by a number of criteria, among them
* the issuing country, sail number, ORC reference number, or boat name. Those search results are represented as
* {@link CertificateHandle} objects, obtained from the {@code activecerts} endpoint through the
* {@link #search(CountryCode, Integer, String, String, String, String, boolean)} method. The {@code includeInvalid}
* parameter is retained for API compatibility; the {@code activecerts} endpoint returns active (valid) certificates
* only, so every handle returned is {@link CertificateHandle#isValid() valid} and resolvable into a full
* {@link ORCCertificate} via {@link #getCertificate(String, CertificateFamily)} (which uses the
* {@link #search(CountryCode, Integer, String, String, String, String)} method. The {@code activecerts} endpoint
* returns active (valid) certificates only, so every handle returned is {@link CertificateHandle#isValid() valid} and
* resolvable into a full {@link ORCCertificate} via {@link #getCertificate(String, CertificateFamily)} (which uses the
* {@code DownBoatRMS_byrefno} endpoint) or {@link #getCertificates(Iterable)}.
* <p>
* The per-country/family overview including the certificate counts and VPP years is obtained from the {@code RMS}
@@ -205,19 +204,22 @@ public interface ORCPublicCertificateDatabase {
* results based on that criterion. You can use "%" as wildcards in the {@code yachtName}, {@code sailNumber} and
* {@code boatClassName} parameters.
* <p>
*
* @param includeInvalid
* If {@code true}, handles may have {@link CertificateHandle#isValid()}{@code == false}, meaning that
* such a handle cannot be resolved to a {@link ORCCertificate} using any of
* {@link #getCertificate(String)}, {@link #getCertificates(Iterable)} and
* {@link #getCertificates(CertificateHandle...)}. If {@code false}, only valid certificate handles
* will be returned that should all be resolvable into an {@link ORCCertificate} with any of the methods
* listed above.
*
* This search runs against the {@code activecerts} endpoint and therefore returns <em>currently active
* certificates only</em>; inactive, expired or superseded certificates are never returned, regardless of the
* criteria. In particular, searching by {@code referenceNumber} here will <em>not</em> find an inactive
* certificate even though the reference number identifies it exactly. The only way this adapter reaches an
* inactive certificate is to download it by its reference number through
* {@link #getCertificate(String, CertificateFamily)} (backed by the {@code DownBoatRMS_byrefno} endpoint, which
* serves a certificate whether or not it is still active); {@link #getCertificateHandle(String)} does the same to
* obtain a {@link CertificateHandle}. There is no search over inactive certificates in this adapter. Every handle
* returned by this method is {@link CertificateHandle#isValid() valid} and resolvable into an
* {@link ORCCertificate} via {@link #getCertificate(String, CertificateFamily)} or
* {@link #getCertificates(Iterable)}.
*
* @return never {@code null}; an object which may be {@link Util#isEmpty() empty}.
*/
Iterable<CertificateHandle> search(CountryCode country, Integer yearOfIssuance, String referenceNumber,
String yachtName, String sailNumber, String boatClassName, boolean includeInvalid) throws Exception;
String yachtName, String sailNumber, String boatClassName) throws Exception;
Iterable<ORCCertificate> getCertificates(Iterable<CertificateHandle> handles) throws Exception;
@@ -226,7 +228,7 @@ public interface ORCPublicCertificateDatabase {
}
default CertificateHandle getCertificateHandle(String referenceNumber) throws Exception {
return search(null, null, referenceNumber, null, null, null, /* includeInvalid */ true).iterator().next();
return search(null, null, referenceNumber, null, null, null).iterator().next();
}
ORCCertificate searchForUpdate(ORCCertificate certificate) throws Exception;
@@ -79,6 +79,7 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
private static final String ENDPOINT_ACTIVECERTS = "activecerts";
private static final String ENDPOINT_RMS = "RMS";
private static final String ENDPOINT_DOWNBOATRMS_BYREFNO = "DownBoatRMS_byrefno";
private static final String ENDPOINT_DOWNRMS = "DownRMS";
private static final String EXT_JSON = "ext=json";
private static final String DATA_KEY = "data";
@@ -352,8 +353,7 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
@Override
public Iterable<CertificateHandle> search(final CountryCode issuingCountry, final Integer yearOfIssuance,
final String referenceNumber, final String yachtName, final String sailNumber, final String boatClassName,
final boolean includeInvalid /* retained for API compatibility; activecerts returns valid certs only */)
final String referenceNumber, final String yachtName, final String sailNumber, final String boatClassName)
throws IOException {
final Set<CertificateHandle> result = new HashSet<>();
final List<String> queryParts = new ArrayList<>();
@@ -418,6 +418,29 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
return result;
}
/**
* Resolves a certificate handle directly from its reference number through the {@code DownBoatRMS_byrefno} download
* endpoint rather than through the active-certificate {@link #search(CountryCode, Integer, String, String, String,
* String, boolean) search}. The {@code activecerts} search only ever returns currently active certificates, whereas
* this endpoint also serves inactive/expired certificates, so this override preserves the pre-key-API behavior that
* a handle could be obtained for any certificate addressable by its reference number. Returns {@code null} if no
* certificate exists for the given reference number.
*/
@Override
public CertificateHandle getCertificateHandle(final String referenceNumber) throws IOException {
final ORCCertificate certificate = getCertificate(referenceNumber, CertificateFamily.UNKNOWN);
return certificate == null ? null : handleFromCertificate(certificate);
}
private CertificateHandle handleFromCertificate(final ORCCertificate certificate) {
final Double gph = certificate.getGPH() == null ? null : certificate.getGPH().asSeconds();
return new CertificateHandleImpl(certificate.getIssuingCountry(), certificate.getFileId(), gph,
/* sssid */ null, /* datInGID */ null, certificate.getReferenceNumber(), CertificateFamily.UNKNOWN,
certificate.getBoatName(), certificate.getSailNumber(), certificate.getBoatClassName(),
/* designer */ null, /* builder */ null, /* yearBuilt */ null, certificate.getIssueDate(),
/* certType */ null, /* oneDesign */ null, /* isProvisional */ Boolean.FALSE, /* isValid */ Boolean.TRUE);
}
@Override
public ORCCertificate searchForUpdate(final CertificateHandle certificateHandle) throws IOException {
return searchForUpdate(certificateHandle.getReferenceNumber());
@@ -457,6 +480,19 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
return result;
}
/**
* Opens a stream on the bulk per-country certificate download provided by the {@code DownRMS} endpoint for the
* given {@code country}, in the JSON format consumed by {@link ORCCertificatesJsonImporter#read(InputStream)}. All
* endpoint knowledge (base URL, endpoint name, {@code ext} parameter) and the API key live in this implementation
* only; the key is read from the {@value #API_KEY_ENV_VARIABLE_NAME} environment variable and never logged. This
* method is exported to the test bundle via an {@code x-friends} restriction so that tests can obtain the online
* bulk stream without duplicating the URL construction or the key handling.
*/
public InputStream openBulkCertificatesJsonStream(final CountryCode country) throws IOException {
return httpGetStream(endpointUrl(requireApiKey(), ENDPOINT_DOWNRMS,
KEY_COUNTRY_ID + "=" + encode(country.getThreeLetterIOCCode()) + "&" + EXT_JSON));
}
private CertificateHandle parseHandleFromActiveCertRow(final JSONObject row) throws DateTimeParseException {
final CountryCode issuingCountry = countryFromIoc(asTrimmedString(row.get(KEY_COUNTRY_ID)));
final String dxtName = asString(row.get(KEY_DXT_NAME));
@@ -723,13 +759,13 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
*/
private Iterable<CertificateHandle> fuzzySearchVaryingBoatClassName(final String yachtName, final String sailNumber, final BoatClass boatClass) throws Exception {
String successfulBoatClassName = boatClass==null?null:boatClass.getName();
Iterable<CertificateHandle> certificateHandles = search(/* issuingCountry */ null, /* yearOfIssuance */ null, /* referenceNumber */ null, yachtName, sailNumber, successfulBoatClassName, /* includeInvalid */ false);
Iterable<CertificateHandle> certificateHandles = search(/* issuingCountry */ null, /* yearOfIssuance */ null, /* referenceNumber */ null, yachtName, sailNumber, successfulBoatClassName);
if (!containsValidHandle(certificateHandles) && successfulBoatClassName != null) {
if (yachtName != null || sailNumber != null) {
logger.fine(()->"Nothing found; removing boat class name restriction "+boatClass.getName());
// try without boat class restriction
successfulBoatClassName = null;
certificateHandles = search(/* issuingCountry */ null, /* yearOfIssuance */ null, /* referenceNumber */ null, yachtName, sailNumber, successfulBoatClassName, /* includeInvalid */ false);
certificateHandles = search(/* issuingCountry */ null, /* yearOfIssuance */ null, /* referenceNumber */ null, yachtName, sailNumber, successfulBoatClassName);
} else {
logger.fine(()->"No current certificates found for boat class "+boatClass.getName()+
" but yacht name and sail number are not specified either; giving up.");
@@ -5733,7 +5733,7 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet
String referenceNumber, String yachtName, String sailNumber, String boatClassName) throws Exception {
final ORCPublicCertificateDatabase db = ORCPublicCertificateDatabase.INSTANCE;
final Set<ORCCertificate> result = new HashSet<>();
final Iterable<CertificateHandle> searchResult = db.search(country, yearOfIssuance, referenceNumber, yachtName, sailNumber, boatClassName, /* includeInvalid */ false);
final Iterable<CertificateHandle> searchResult = db.search(country, yearOfIssuance, referenceNumber, yachtName, sailNumber, boatClassName);
Util.addAll(
db.getCertificates(searchResult),
result);
+5
View File
@@ -141,3 +141,8 @@ if [ -n "${GEONAMES_ORG_USERNAMES}" ]; then
ADDITIONAL_JAVA_ARGS="${ADDITIONAL_JAVA_ARGS} -Dgeonames.org.usernames=${GEONAMES_ORG_USERNAMES}"
fi
export GITHUB_TOKEN
# Export the ORC public-certificate API key so the Java VM can read it via System.getenv("ORC_API_KEY").
# configuration/secrets sets it as a plain (non-exported) shell variable, which the java child process would
# not otherwise inherit. Unlike the other ORC-adjacent credentials it is passed as an environment variable
# rather than a -D system property so that it is not exposed on the process command line (ps auxww).
export ORC_API_KEY