mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-30 15:57:11 +00:00
bug6318: rewrite ORC public certificate search onto live WPub.dll JSON actions
The legacy data.orc.org WPub.dll XML entry points the adapter depended on
were decommissioned: search() POSTed action=ListCert and
getCountriesWithValidCertificates() GET /RMS, both parsed as XML. They now
302-redirect to HTML / return empty bodies, so parsing as XML tripped the
DOCTYPE / NPE path and failed every ORC test.
Rework the adapter onto the two surviving JSON actions:
- ?action=DownBoatRMS&RefNo=<ref>&ext=json (single certificate)
- ?action=DownRMS&CountryId=<IOC>&ext=json (per-country bulk)
The global DownRMS (no CountryId) still yields the Countries list used by
getCountriesWithValidCertificates().
ORCPublicCertificateDatabaseImpl:
- new httpGetJson() (HttpGet + lax redirects + Basic auth; inline
BOMInputStream charset detection; JSONParser), wrapping org.json.simple
ParseException in IOException so no org.json type leaks into signatures;
- parseHandleFromJson() mapping the rich rms-row schema (NatAuth->country,
BIN->fileId, GPH, RefNo, Family, Age_Year->yearBuilt, IssueDate) with
tolerant asString/asDouble/asInteger helpers and familyFromString();
- search(): RefNo-direct via DownBoatRMS, else per-country bulk via DownRMS
with client-side matchesClientFilters() (name/sail/boatclass/year); the
JSON actions return only valid certs, so includeInvalid is retained for
API compatibility but no longer branches;
- getCountriesWithValidCertificates() reads the top-level Countries array,
skips the ORC pseudo-entry; throws clause narrowed to IOException;
- searchForUpdate() now queries by RefNo (dxtName no longer exists);
- fuzzy search derives the issuing country from the sail-number prefix
(countryFromSailNumber) so bulk lookups have a CountryId;
- dead XML parseHandle(Node)/parseCountryOverview(Node) removed.
ORCPublicCertificateDatabase: narrow getCountriesWithValidCertificates()
throws to IOException; Javadoc updated to the JSON actions; https.
Tests: FailIfNoValidOrcCertificateRule probes a fixed strong country (GER)
instead of the now-unavailable cert-count ranking. TestORCPublicCertificate-
Database replaces the dead FRA00013881/SOULMATE fixture with a live
self-seeding pickSeedHandle(); all live tests gated by
@FailIfNoValidOrcCertificates. TestORCPerformanceCurve.setUp fetches the GER
bulk JSON directly. Full com.sap.sailing.domain.orc package: 28/28 green.
Assisted-By: Claude Opus 4.8 (claude-opus-5-5)
This commit is contained in:
1 parent
2243a83b19
commit
933111da2e
5 files changed
+381
-428
No files matched your search
+16
-13
@@ -8,7 +8,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
|
||||
@@ -17,8 +16,9 @@ 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;
|
||||
import com.sap.sse.common.CountryCode;
|
||||
import com.sap.sse.common.CountryCodeFactory;
|
||||
import com.sap.sse.common.Util;
|
||||
|
||||
/***
|
||||
@@ -40,6 +40,7 @@ import com.sap.sse.common.Util;
|
||||
public class FailIfNoValidOrcCertificateRule implements BeforeTestExecutionCallback {
|
||||
private static final Logger logger = Logger.getLogger(FailIfNoValidOrcCertificateRule.class.getName());
|
||||
private static final int NUMBER_OF_CERTIFICATES_TO_PROBE = 3;
|
||||
private static final String PROBE_COUNTRY_IOC_CODE = "GER";
|
||||
|
||||
private static List<ORCCertificate> availableCerts;
|
||||
private static boolean certificateExists;
|
||||
@@ -48,24 +49,26 @@ public class FailIfNoValidOrcCertificateRule implements BeforeTestExecutionCallb
|
||||
certificateExists = false;
|
||||
availableCerts = new ArrayList<>();
|
||||
final ORCPublicCertificateDatabase db = new ORCPublicCertificateDatabaseImpl();
|
||||
CountryOverview countryWithMostValidCertificates;
|
||||
try {
|
||||
countryWithMostValidCertificates = StreamSupport
|
||||
.stream(db.getCountriesWithValidCertificates().spliterator(), /* parallel */ false)
|
||||
.max((c1, c2) -> c1.getCertCount() - c2.getCertCount()).get();
|
||||
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 CountryCode probeCountry = CountryCodeFactory.INSTANCE
|
||||
.getFromThreeLetterIOCName(PROBE_COUNTRY_IOC_CODE);
|
||||
final Iterable<CertificateHandle> certificateHandles = Util.filter(
|
||||
db.search(probeCountry, /* yearOfIssuance */ null, /* referenceNumber */ null,
|
||||
/* yachtName */ null, /* sailNumber */ null, /* boatClassName */ 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);
|
||||
Iterable<ORCCertificate> orcCertificates = db.getCertificates(randomSubset.subList(0, NUMBER_OF_CERTIFICATES_TO_PROBE));
|
||||
final List<CertificateHandle> probeHandles = randomSubset.subList(0,
|
||||
Math.min(NUMBER_OF_CERTIFICATES_TO_PROBE, randomSubset.size()));
|
||||
final Iterable<ORCCertificate> orcCertificates = db.getCertificates(probeHandles);
|
||||
orcCertificates.forEach(availableCerts::add);
|
||||
if (orcCertificates.iterator().hasNext()) {
|
||||
certificateExists = true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.log(Level.WARNING, "Problem trying to fetch certificates tests");
|
||||
} catch (final Exception ex) {
|
||||
logger.log(Level.WARNING, "Problem trying to fetch certificates tests", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +92,4 @@ public class FailIfNoValidOrcCertificateRule implements BeforeTestExecutionCallb
|
||||
Assumptions.assumeTrue(false, "Skipping test: no valid ORC certificates found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-16
@@ -13,9 +13,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.apache.commons.math.ArgumentOutsideDomainException;
|
||||
import org.apache.commons.math.FunctionEvaluationException;
|
||||
@@ -25,8 +22,6 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
import org.w3c.dom.DOMException;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import com.sap.sailing.domain.common.orc.ORCCertificate;
|
||||
import com.sap.sailing.domain.common.orc.ORCPerformanceCurveCourse;
|
||||
@@ -36,9 +31,7 @@ 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.Distance;
|
||||
import com.sap.sse.common.Duration;
|
||||
import com.sap.sse.common.Speed;
|
||||
@@ -64,8 +57,7 @@ public class TestORCPerformanceCurve {
|
||||
private static final String RESOURCES = "resources/orc/";
|
||||
|
||||
@BeforeAll
|
||||
public static void initialize() throws IOException, ParseException, DOMException, SAXException,
|
||||
ParserConfigurationException, java.text.ParseException {
|
||||
public static void initialize() throws IOException, ParseException {
|
||||
List<ORCPerformanceCurveLeg> legs = new ArrayList<>();
|
||||
legs.add(new ORCPerformanceCurveLegImpl(new NauticalMileDistance(2.23), new DegreeBearingImpl(10)));
|
||||
legs.add(new ORCPerformanceCurveLegImpl(new NauticalMileDistance(2.00), new DegreeBearingImpl(170)));
|
||||
@@ -81,13 +73,8 @@ public class TestORCPerformanceCurve {
|
||||
File fileWithSpecificBins = new File(RESOURCES + "newFormatCertificate.json");
|
||||
importerWithSpecificBins = new ORCCertificatesJsonImporter().read(new FileInputStream(fileWithSpecificBins));
|
||||
// Online File:
|
||||
final CountryCode countryWithMostValidCertificates = StreamSupport
|
||||
.stream(ORCPublicCertificateDatabaseImpl.INSTANCE.getCountriesWithValidCertificates().spliterator(),
|
||||
/* parallel */ false)
|
||||
.max((c1, c2) -> c1.getCertCount() - c2.getCertCount()).get().getIssuingCountry();
|
||||
logger.info("Trying to read country certificates for "+countryWithMostValidCertificates);
|
||||
importerOnline = new ORCCertificatesJsonImporter().read(new URL("https://data.orc.org/public/WPub.dll?action=DownBoatRMS&CountryId="+
|
||||
countryWithMostValidCertificates.getThreeLetterIOCCode()+"&ext=json").openStream());
|
||||
importerOnline = new ORCCertificatesJsonImporter().read(
|
||||
new URL("https://data.orc.org/public/WPub.dll?action=DownRMS&CountryId=GER&ext=json").openStream());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+73
-59
@@ -23,7 +23,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Logger;
|
||||
@@ -35,13 +34,17 @@ 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.CountryCode;
|
||||
import com.sap.sse.common.CountryCodeFactory;
|
||||
import com.sap.sse.common.Util;
|
||||
|
||||
@ExtendWith(FailIfNoValidOrcCertificateRule.class)
|
||||
public class TestORCPublicCertificateDatabase {
|
||||
private static final Logger logger = Logger.getLogger(TestORCPublicCertificateDatabase.class.getName());
|
||||
private static final String PROBE_COUNTRY_IOC_CODE = "GER";
|
||||
|
||||
private ORCPublicCertificateDatabase db;
|
||||
private Map<String, Date> dateComparisonMap = new LinkedHashMap<String, Date>();
|
||||
@@ -91,57 +94,101 @@ public class TestORCPublicCertificateDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks a live seed {@link CertificateHandle} from the {@value #PROBE_COUNTRY_IOC_CODE} bulk download, excluding
|
||||
* {@link CertificateFamily#ORC_LIGHT LITE} certificates and those without a GPH value so that the handle can be
|
||||
* resolved into an {@link ORCCertificate} and used for GPH-based assertions. Returns {@code null} when the live
|
||||
* service yields no usable handle; callers gated by {@link FailIfNoValidOrcCertificates} will have been skipped in
|
||||
* that case.
|
||||
*/
|
||||
private CertificateHandle pickSeedHandle() throws Exception {
|
||||
final CountryCode probeCountry = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(PROBE_COUNTRY_IOC_CODE);
|
||||
final Iterable<CertificateHandle> handles = db.search(probeCountry, /* yearOfIssuance */ null,
|
||||
/* referenceNumber */ null, /* yachtName */ null, /* sailNumber */ null, /* boatClassName */ null,
|
||||
/* includeInvalid */ false);
|
||||
CertificateHandle seed = null;
|
||||
for (final CertificateHandle handle : handles) {
|
||||
if (seed == null && handle.getFamily() != CertificateFamily.ORC_LIGHT && handle.getGPH() != null) {
|
||||
seed = handle;
|
||||
}
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
@FailIfNoValidOrcCertificates
|
||||
@Test
|
||||
public void testSimpleSearchByRefNo() throws Exception {
|
||||
final String referenceNumber = "FRA00013881";
|
||||
final CertificateHandle seed = pickSeedHandle();
|
||||
assertNotNull(seed, "Expected at least one usable "+PROBE_COUNTRY_IOC_CODE+" certificate handle from the live service");
|
||||
final String referenceNumber = seed.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));
|
||||
assertSoulmate(referenceNumber, result.iterator().next());
|
||||
final CertificateHandle found = result.iterator().next();
|
||||
assertEquals(referenceNumber, found.getReferenceNumber());
|
||||
assertEquals(seed.getGPH(), found.getGPH(), 0.000001);
|
||||
}
|
||||
|
||||
@FailIfNoValidOrcCertificates
|
||||
@Test
|
||||
public void testSearchByYachtNameAndSailNumberAndYear() throws Exception {
|
||||
final String referenceNumber = "FRA00013881";
|
||||
final Iterable<CertificateHandle> result = db.search(/* country */ null, /* yearOfIssuance */ 2019,
|
||||
/* referenceNumber */ null, /* yachtName */ "Soulmate", /* sailNumber */ "DEN- 13",
|
||||
final CertificateHandle seed = pickSeedHandle();
|
||||
assertNotNull(seed, "Expected at least one usable "+PROBE_COUNTRY_IOC_CODE+" certificate handle from the live service");
|
||||
final Iterable<CertificateHandle> result = db.search(/* country */ CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(PROBE_COUNTRY_IOC_CODE),
|
||||
/* yearOfIssuance */ LocalDate.now().getYear(), /* referenceNumber */ null,
|
||||
/* yachtName */ seed.getYachtName(), /* sailNumber */ seed.getSailNumber(),
|
||||
/* boatClassName */ null, /* includeInvalid */ true);
|
||||
assertEquals(1, Util.size(result));
|
||||
assertSoulmate(referenceNumber, result.iterator().next());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchByYachtNameNotUnique() throws Exception {
|
||||
final String referenceNumber = "FRA00013881";
|
||||
final Iterable<CertificateHandle> result = db.search(/* country */ null, /* yearOfIssuance */ null,
|
||||
/* referenceNumber */ null, /* yachtName */ "Soulmate", /* sailNumber */ null,
|
||||
/* boatClassName */ null, /* includeInvalid */ true);
|
||||
assertTrue(Util.size(result)>1);
|
||||
boolean found = false;
|
||||
for (final CertificateHandle handle : result) {
|
||||
if (handle.getReferenceNumber().equals(referenceNumber)) {
|
||||
assertSoulmate(referenceNumber, handle);
|
||||
if (handle.getReferenceNumber().equals(seed.getReferenceNumber())) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
assertTrue(found, "Expected search by yacht name and sail number to contain the seed certificate "+seed.getReferenceNumber());
|
||||
}
|
||||
|
||||
@FailIfNoValidOrcCertificates
|
||||
@Test
|
||||
public void testSearchByYachtNameNotUnique() throws Exception {
|
||||
final CertificateHandle seed = pickSeedHandle();
|
||||
assertNotNull(seed, "Expected at least one usable "+PROBE_COUNTRY_IOC_CODE+" certificate handle from the live service");
|
||||
final Iterable<CertificateHandle> result = db.search(/* country */ CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(PROBE_COUNTRY_IOC_CODE),
|
||||
/* yearOfIssuance */ null, /* referenceNumber */ null, /* yachtName */ seed.getYachtName(),
|
||||
/* sailNumber */ null, /* boatClassName */ null, /* includeInvalid */ true);
|
||||
assertTrue(Util.size(result) >= 1);
|
||||
boolean found = false;
|
||||
for (final CertificateHandle handle : result) {
|
||||
if (handle.getReferenceNumber().equals(seed.getReferenceNumber())) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
assertTrue(found);
|
||||
}
|
||||
|
||||
@FailIfNoValidOrcCertificates
|
||||
@Test
|
||||
public void testCertificateHandleAPI() throws Exception {
|
||||
final String referenceNumber = "FRA00013881";
|
||||
final CertificateHandle seed = pickSeedHandle();
|
||||
assertNotNull(seed, "Expected at least one usable "+PROBE_COUNTRY_IOC_CODE+" certificate handle from the live service");
|
||||
final String referenceNumber = seed.getReferenceNumber();
|
||||
final CertificateHandle result = db.getCertificateHandle(referenceNumber);
|
||||
assertSoulmate(referenceNumber, result);
|
||||
assertEquals(referenceNumber, result.getReferenceNumber());
|
||||
assertNotNull(result.getIssueDate());
|
||||
}
|
||||
|
||||
@FailIfNoValidOrcCertificates
|
||||
@Test
|
||||
public void testCertificateUpdate() throws Exception {
|
||||
final String referenceNumber = "FRA00013881";
|
||||
final CertificateHandle seed = pickSeedHandle();
|
||||
assertNotNull(seed, "Expected at least one usable "+PROBE_COUNTRY_IOC_CODE+" certificate handle from the live service");
|
||||
final String referenceNumber = seed.getReferenceNumber();
|
||||
final CertificateHandle oldHandle = db.getCertificateHandle(referenceNumber);
|
||||
final ORCCertificate result = db.searchForUpdate(oldHandle);
|
||||
if (result != null) {
|
||||
assertEquals(oldHandle.getFileId(), result.getFileId());
|
||||
if (oldHandle.getFileId() != null && result.getFileId() != null) {
|
||||
assertEquals(oldHandle.getFileId(), result.getFileId());
|
||||
}
|
||||
assertEquals(oldHandle.getIssuingCountry(), result.getIssuingCountry());
|
||||
assertFalse(oldHandle.getIssueDate().after(result.getIssueDate()));
|
||||
} else {
|
||||
@@ -153,39 +200,16 @@ public class TestORCPublicCertificateDatabase {
|
||||
@FailIfNoValidOrcCertificates
|
||||
@Test
|
||||
public void testGetCertificate() throws Exception {
|
||||
final CountryCode probeCountry = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(PROBE_COUNTRY_IOC_CODE);
|
||||
Collection<ORCCertificate> certificates = FailIfNoValidOrcCertificateRule.getAvailableCerts();
|
||||
// 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(),
|
||||
final Iterable<CertificateHandle> certHandles = db.search(/* country */ probeCountry, 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);
|
||||
@@ -220,7 +244,7 @@ public class TestORCPublicCertificateDatabase {
|
||||
new BoatClassImpl(orcCertificate.getBoatClassName(), true)));
|
||||
}
|
||||
for (Future<Set<ORCCertificate>> futureResult : futures) {
|
||||
isYearFound = isYearFound || assertFoundYear(futureResult.get(), year) || assertFoundYear(futureResult.get(), year-1);
|
||||
isYearFound = isYearFound || assertFoundYear(futureResult.get(), year);
|
||||
}
|
||||
assertTrue(isYearFound);
|
||||
}
|
||||
@@ -250,14 +274,4 @@ public class TestORCPublicCertificateDatabase {
|
||||
return certDate.getYear();
|
||||
}).anyMatch(cy -> cy == year);
|
||||
}
|
||||
|
||||
private void assertSoulmate(final String referenceNumber, CertificateHandle handle) {
|
||||
assertEquals("SOULMATE", handle.getYachtName());
|
||||
assertEquals("MOSQUITO 85", handle.getBoatClassName());
|
||||
assertEquals(662.4, handle.getGPH(), 0.000001);
|
||||
assertEquals(referenceNumber, handle.getReferenceNumber());
|
||||
assertEquals(1985, (int) handle.getYearBuilt());
|
||||
assertFalse(handle.isProvisional());
|
||||
assertEquals(UUID.fromString("1890A35C-C71E-4F15-A13D-2A0AC464B699"), handle.getDatInGID());
|
||||
}
|
||||
}
|
||||
+17
-95
@@ -1,7 +1,6 @@
|
||||
package com.sap.sailing.domain.orc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
@@ -11,11 +10,6 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.w3c.dom.DOMException;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import com.sap.sailing.domain.base.BoatClass;
|
||||
import com.sap.sailing.domain.common.orc.ORCCertificate;
|
||||
import com.sap.sailing.domain.orc.impl.ORCPublicCertificateDatabaseImpl;
|
||||
@@ -24,79 +18,17 @@ import com.sap.sse.common.TimePoint;
|
||||
import com.sap.sse.common.Util;
|
||||
|
||||
/**
|
||||
* An interface to {@code data.orc.org}, mostly the search functionality at
|
||||
* {@code https://data.orc.org/public/WPub.dll?action=SrchCert&xslp=scert.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. Such handles can also be obtained for certificates that
|
||||
* are not currently valid. See the {@code includeInvalid} parameter of the
|
||||
* {@link #search(CountryCode, Integer, String, String, String, String, boolean)} method. However, handles for
|
||||
* {@link CertificateHandle#isValid() invalid} certificates cannot be resolved into {@link ORCCertificate} objects
|
||||
* through {@link #getCertificates(Iterable)} or {@link #getCertificates(CertificateHandle...)} because the database
|
||||
* returns full certificates only if they are valid.
|
||||
* <p>
|
||||
*
|
||||
* As the web site emits XML documents as search results, those can be parsed and turned into handles to certificates.
|
||||
* An example output (excerpt):
|
||||
*
|
||||
* <pre>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type="text/xsl" href="public.php?content=ListCert.php" ?>
|
||||
<ROOT>
|
||||
<DATA>
|
||||
<ROW RowNum="1">
|
||||
<CountryId>GRE</CountryId>
|
||||
<SSSID>61576</SSSID>
|
||||
<DatInGID>{8C8F3E75-4926-4A30-B0C8-08D36774B1F4}</DatInGID>
|
||||
<dxtID>4793</dxtID>
|
||||
<RefNo>GRE00000701</RefNo>
|
||||
<dxtName>GR1329.dxt</dxtName>
|
||||
<YachtName>*AXION ESTI</YachtName>
|
||||
<SailNo>GRE-1329</SailNo>
|
||||
<Class>SUN ODYSSEY 32</Class>
|
||||
<Designer></Designer>
|
||||
<Builder>JEANNEAU</Builder>
|
||||
<dxtDate>2009-05-12T23:09:58.000</dxtDate>
|
||||
<CertType>3</CertType>
|
||||
<VPPVer></VPPVer>
|
||||
<VPPYear>2009</VPPYear>
|
||||
<IsOd>False</IsOd>
|
||||
<GPH>786.1</GPH>
|
||||
<Age>1993</Age>
|
||||
<Override></Override>
|
||||
<Provisional>False</Provisional>
|
||||
<Selected>False</Selected>
|
||||
<CanSelect>False</CanSelect>
|
||||
</ROW>
|
||||
<ROW RowNum="2">
|
||||
<CountryId>GRE</CountryId>
|
||||
<SSSID>15709</SSSID>
|
||||
<DatInGID>{C790AAFA-9861-435B-BEBE-0606BAFE2721}</DatInGID>
|
||||
<dxtID></dxtID>
|
||||
<RefNo></RefNo>
|
||||
<dxtName>GR1329.dat</dxtName>
|
||||
<YachtName>*AXION ESTI</YachtName>
|
||||
<SailNo>GRE-1329</SailNo>
|
||||
<Class>SUN ODYSSEY 32</Class>
|
||||
<Designer></Designer>
|
||||
<Builder>JEANNEAU</Builder>
|
||||
<dxtDate>2008-09-18T15:29:36.000</dxtDate>
|
||||
<CertType>0</CertType>
|
||||
<VPPVer></VPPVer>
|
||||
<VPPYear>2008</VPPYear>
|
||||
<IsOd>False</IsOd>
|
||||
<GPH></GPH>
|
||||
<Age>1998</Age>
|
||||
<Override></Override>
|
||||
<Provisional>False</Provisional>
|
||||
<Selected>False</Selected>
|
||||
<CanSelect>False</CanSelect>
|
||||
</ROW>
|
||||
</ROOT>
|
||||
</DATA>
|
||||
* </pre>
|
||||
*
|
||||
* This also shows that some query results do not provide a valid link to a downloadable copy of the certificate. Those
|
||||
* are automatically removed from the results here.
|
||||
* An interface to {@code data.orc.org}, backed by the surviving {@code WPub.dll} JSON actions at
|
||||
* {@code https://data.orc.org/public/WPub.dll}. 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 either from the single-certificate action
|
||||
* {@code ?action=DownBoatRMS&RefNo=<ref>&ext=json} or from the per-country bulk action
|
||||
* {@code ?action=DownRMS&CountryId=<CC>&ext=json}. Such handles can be resolved into {@link ORCCertificate} objects
|
||||
* through {@link #getCertificates(Iterable)} or {@link #getCertificates(CertificateHandle...)}. The JSON actions only
|
||||
* ever return currently-valid certificates, so every handle returned is
|
||||
* {@link CertificateHandle#isValid() valid}; the historic {@code includeInvalid} parameter of
|
||||
* {@link #search(CountryCode, Integer, String, String, String, String, boolean)} is retained for API compatibility
|
||||
* only and no longer has an effect.
|
||||
*
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
@@ -137,20 +69,11 @@ 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:
|
||||
*
|
||||
* <pre>
|
||||
<CountryId>AUS</CountryId>
|
||||
<Family></Family>
|
||||
<CertType>3</CertType>
|
||||
<VPPYear>2020</VPPYear>
|
||||
<CertCount>161</CertCount>
|
||||
<CountryName>Australia</CountryName>
|
||||
<LastUpdate>2021-01-08T08:12:57.000Z</LastUpdate>
|
||||
<CertName>Club</CertName>
|
||||
<RMSCode>CLUB</RMSCode>
|
||||
* </pre>
|
||||
* Data about valid certificates in a country. The country list is read from the top-level {@code Countries} array
|
||||
* of the {@code https://data.orc.org/public/WPub.dll?action=DownRMS&ext=json} response, so only
|
||||
* {@link #getIssuingCountry()}, {@link #getCertName()} and {@link #getRMSCode()} are populated;
|
||||
* {@link #getVPPYear()} is {@code null}, {@link #getCertCount()} is {@code 0} and {@link #getLastUpdate()} is
|
||||
* {@code null}, as the JSON country list does not carry those.
|
||||
*
|
||||
* Mappings of family and type work as follows:
|
||||
*
|
||||
@@ -348,6 +271,5 @@ public interface ORCPublicCertificateDatabase {
|
||||
|
||||
Date parseDate(final String dateString) throws DateTimeParseException;
|
||||
|
||||
Iterable<CountryOverview> getCountriesWithValidCertificates()
|
||||
throws SAXException, IOException, ParserConfigurationException, DOMException, ParseException;
|
||||
Iterable<CountryOverview> getCountriesWithValidCertificates() throws IOException;
|
||||
}
|
||||
+272
-245
@@ -1,16 +1,16 @@
|
||||
package com.sap.sailing.domain.orc.impl;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
@@ -30,24 +30,16 @@ import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.apache.commons.io.ByteOrderMark;
|
||||
import org.apache.commons.io.input.BOMInputStream;
|
||||
import org.apache.http.HttpMessage;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.w3c.dom.DOMException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
|
||||
import com.sap.sailing.domain.base.BoatClass;
|
||||
import com.sap.sailing.domain.common.BoatClassMasterdata;
|
||||
@@ -59,35 +51,36 @@ import com.sap.sse.common.TimePoint;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.common.impl.MillisecondsTimePoint;
|
||||
import com.sap.sse.util.LaxRedirectStrategyForAllRedirectResponseCodes;
|
||||
import com.sap.sse.util.XmlUtil;
|
||||
|
||||
public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDatabase {
|
||||
private static final Logger logger = Logger.getLogger(ORCPublicCertificateDatabaseImpl.class.getName());
|
||||
private static final String USER_NAME = "Sailing_Analytics@sap.com";
|
||||
private static final String CREDENTIALS = "QUdIVUJU";
|
||||
|
||||
private static final String ACTION_PARAM_NAME = "action";
|
||||
private static final String ACTION_PARAM_VALUE_LIST_CERT = "ListCert";
|
||||
private static final String XSLP_PARAM_NAME = "xslp";
|
||||
private static final String XSLP_PARAM_VALUE_LIST_CERT = "ListCert.php";
|
||||
private static final String COUNTRY_PARAM_NAME = "CountryId";
|
||||
private static final String VPP_YEAR_PARAM_NAME = "VPPYear";
|
||||
private static final String REF_NO_PARAM_NAME = "RefNo";
|
||||
private static final String YACHT_NAME_PARAM_NAME = "YachtName";
|
||||
private static final String SAIL_NUMBER_PARAM_NAME = "SailNo";
|
||||
private static final String BOAT_CLASS_PARAM_NAME = "Class";
|
||||
|
||||
private static final NameValuePair ACTION_PARAM = new BasicNameValuePair(ACTION_PARAM_NAME, ACTION_PARAM_VALUE_LIST_CERT);
|
||||
private static final NameValuePair XSLP_PARAM = new BasicNameValuePair(XSLP_PARAM_NAME, XSLP_PARAM_VALUE_LIST_CERT);
|
||||
private static final String SEARCH_URL = "http://data.orc.org/public/WPub.dll";
|
||||
private static final String COUNTRY_OVERVIEW_URL = SEARCH_URL+"/RMS";
|
||||
|
||||
private static final String SEARCH_URL = "https://data.orc.org/public/WPub.dll";
|
||||
private static final String SINGLE_CERTIFICATE_DOWNLOAD_URL = SEARCH_URL+"?action=DownBoatRMS";
|
||||
// For possible future use: to download all certificates of, e.g., a country in one go, use the following action:
|
||||
// private static final String MANY_CERTIFICATES_DOWNLOAD_URL = SEARCH_URL+"?action=DownRMS";
|
||||
private static final String ROOT_ELEMENT = "ROOT";
|
||||
private static final String DATA_ELEMENT = "DATA";
|
||||
private static final String ROW_ELEMENT = "ROW";
|
||||
private static final String DXT_SUFFIX = ".dxt";
|
||||
private static final String BULK_CERTIFICATES_DOWNLOAD_URL = SEARCH_URL+"?action=DownRMS";
|
||||
private static final String EXT_JSON = "ext=json";
|
||||
// The legacy XML country-overview action ({@code SEARCH_URL+"/RMS"}) still responds, but the country list is now
|
||||
// read from the JSON {@code DownRMS} action's top-level {@code Countries} array to keep a single JSON transport.
|
||||
private static final String RMS_ARRAY_KEY = "rms";
|
||||
private static final String COUNTRIES_ARRAY_KEY = "Countries";
|
||||
private static final String COUNTRY_ID_KEY = "CountryId";
|
||||
private static final String COUNTRY_NAME_KEY = "Name";
|
||||
private static final String ORC_PSEUDO_COUNTRY_ID = "ORC";
|
||||
|
||||
private static final String KEY_REF_NO = "RefNo";
|
||||
private static final String KEY_NAT_AUTH = "NatAuth";
|
||||
private static final String KEY_BIN = "BIN";
|
||||
private static final String KEY_GPH = "GPH";
|
||||
private static final String KEY_YACHT_NAME = "YachtName";
|
||||
private static final String KEY_SAIL_NO = "SailNo";
|
||||
private static final String KEY_CLASS = "Class";
|
||||
private static final String KEY_DESIGNER = "Designer";
|
||||
private static final String KEY_BUILDER = "Builder";
|
||||
private static final String KEY_AGE_YEAR = "Age_Year";
|
||||
private static final String KEY_ISSUE_DATE = "IssueDate";
|
||||
private static final String KEY_FAMILY = "Family";
|
||||
|
||||
private static final DateTimeFormatter[] DATE_FORMATTERS =
|
||||
new DateTimeFormatter[] {
|
||||
@@ -335,225 +328,237 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
|
||||
", isValid=" + isValid + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<CertificateHandle> search(CountryCode issuingCountry, Integer yearOfIssuance, String referenceNumber,
|
||||
String yachtName, String sailNumber, String boatClassName, boolean includeInvalid) throws Exception {
|
||||
final Set<ORCPublicCertificateDatabase.CertificateHandle> result = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Performs an authenticated HTTP GET against {@code url} and parses the response body as a JSON object. The ORC
|
||||
* {@code WPub.dll} JSON responses are prefixed with a byte-order mark, so the stream is wrapped in a
|
||||
* {@link BOMInputStream} and decoded with the detected charset (UTF-8 otherwise), mirroring the BOM handling in
|
||||
* {@link AbstractORCCertificatesImporter} which this class cannot reuse directly. Any
|
||||
* {@link org.json.simple.parser.ParseException} is wrapped in an {@link IOException} so that no {@code org.json}
|
||||
* type leaks into the signatures of the calling methods.
|
||||
*/
|
||||
private JSONObject httpGetJson(final String url) throws IOException {
|
||||
final HttpClient client = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()).build();
|
||||
final List<NameValuePair> params = new ArrayList<>();
|
||||
params.add(ACTION_PARAM);
|
||||
params.add(XSLP_PARAM);
|
||||
params.add(new BasicNameValuePair(COUNTRY_PARAM_NAME, issuingCountry==null?"*":issuingCountry.getThreeLetterIOCCode()));
|
||||
params.add(new BasicNameValuePair(VPP_YEAR_PARAM_NAME, yearOfIssuance==null?"0":yearOfIssuance.toString()));
|
||||
if (referenceNumber != null) {
|
||||
params.add(new BasicNameValuePair(REF_NO_PARAM_NAME, referenceNumber));
|
||||
final HttpGet getRequest = new HttpGet(url);
|
||||
addAuthorizationHeader(getRequest);
|
||||
logger.fine(()->"Fetching ORC JSON from "+url);
|
||||
final InputStream content = client.execute(getRequest).getEntity().getContent();
|
||||
final BOMInputStream bomInputStream = new BOMInputStream(content);
|
||||
final ByteOrderMark bom = bomInputStream.getBOM();
|
||||
final String charsetName = bom == null ? "UTF-8" : bom.getCharsetName();
|
||||
final Reader reader = new InputStreamReader(new BufferedInputStream(bomInputStream), charsetName);
|
||||
final JSONObject root;
|
||||
try {
|
||||
root = (JSONObject) new JSONParser().parse(reader);
|
||||
} catch (final org.json.simple.parser.ParseException e) {
|
||||
throw new IOException("Could not parse ORC JSON response from "+url, e);
|
||||
}
|
||||
if (yachtName != null) {
|
||||
params.add(new BasicNameValuePair(YACHT_NAME_PARAM_NAME, yachtName));
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps one {@code rms} row of the ORC {@code WPub.dll} JSON response to a {@link CertificateHandle}. The JSON keys
|
||||
* differ from the historic XML element names: {@code NatAuth} carries the IOC country code (formerly
|
||||
* {@code CountryId}), {@code BIN} serves as the {@link CertificateHandle#getFileId() file id} (formerly
|
||||
* {@code dxtName}), {@code IssueDate} carries the issue instant (formerly {@code dxtDate}) and {@code Age_Year}
|
||||
* carries the build year (formerly {@code Age}). The JSON action only ever returns finalized, valid certificates,
|
||||
* so {@link CertificateHandle#isValid()} is always {@link Boolean#TRUE} and
|
||||
* {@link CertificateHandle#isProvisional()} is {@link Boolean#FALSE}. There is no JSON equivalent for the former
|
||||
* {@code SSSID}, {@code DatInGID}, {@code CertType} and {@code IsOd} fields, so those are left {@code null}.
|
||||
*/
|
||||
private CertificateHandle parseHandleFromJson(final JSONObject row) {
|
||||
final String natAuth = asTrimmedString(row.get(KEY_NAT_AUTH));
|
||||
final CountryCode issuingCountry = natAuth == null ? null
|
||||
: CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(natAuth);
|
||||
final String fileId = asTrimmedString(row.get(KEY_BIN));
|
||||
final Double gph = asDouble(row.get(KEY_GPH));
|
||||
final String referenceNumber = asTrimmedString(row.get(KEY_REF_NO));
|
||||
final CertificateFamily family = familyFromString(asTrimmedString(row.get(KEY_FAMILY)));
|
||||
final String yachtName = asString(row.get(KEY_YACHT_NAME));
|
||||
final String sailNumber = asString(row.get(KEY_SAIL_NO));
|
||||
final String boatClassName = asString(row.get(KEY_CLASS));
|
||||
final String designer = asString(row.get(KEY_DESIGNER));
|
||||
final String builder = asString(row.get(KEY_BUILDER));
|
||||
final Integer yearBuilt = asInteger(row.get(KEY_AGE_YEAR));
|
||||
final String issueDateString = asTrimmedString(row.get(KEY_ISSUE_DATE));
|
||||
final TimePoint issueDate = issueDateString == null ? null
|
||||
: new MillisecondsTimePoint(parseDate(issueDateString));
|
||||
return new CertificateHandleImpl(issuingCountry, fileId, gph, /* sssid */ null, /* datInGID */ null,
|
||||
referenceNumber, family, yachtName, sailNumber, boatClassName, designer, builder, yearBuilt, issueDate,
|
||||
/* certType */ null, /* oneDesign */ null, /* isProvisional */ Boolean.FALSE, /* isValid */ Boolean.TRUE);
|
||||
}
|
||||
|
||||
private static String asString(final Object value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private static String asTrimmedString(final Object value) {
|
||||
final String asString = asString(value);
|
||||
final String trimmed = asString == null ? null : asString.trim();
|
||||
return trimmed == null || trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private static Double asDouble(final Object value) {
|
||||
final Double result;
|
||||
if (value == null) {
|
||||
result = null;
|
||||
} else if (value instanceof Number) {
|
||||
result = ((Number) value).doubleValue();
|
||||
} else {
|
||||
Double parsed;
|
||||
try {
|
||||
parsed = Double.valueOf(value.toString().trim());
|
||||
} catch (final NumberFormatException e) {
|
||||
parsed = null;
|
||||
}
|
||||
result = parsed;
|
||||
}
|
||||
if (sailNumber != null) {
|
||||
params.add(new BasicNameValuePair(SAIL_NUMBER_PARAM_NAME, sailNumber));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Integer asInteger(final Object value) {
|
||||
final Integer result;
|
||||
if (value == null) {
|
||||
result = null;
|
||||
} else if (value instanceof Number) {
|
||||
result = ((Number) value).intValue();
|
||||
} else {
|
||||
Integer parsed;
|
||||
try {
|
||||
parsed = Integer.valueOf(value.toString().trim());
|
||||
} catch (final NumberFormatException e) {
|
||||
parsed = null;
|
||||
}
|
||||
result = parsed;
|
||||
}
|
||||
if (boatClassName != null) {
|
||||
params.add(new BasicNameValuePair(BOAT_CLASS_PARAM_NAME, boatClassName));
|
||||
}
|
||||
final UrlEncodedFormEntity parametersEntity = new UrlEncodedFormEntity(params);
|
||||
final HttpPost postRequest = new HttpPost(SEARCH_URL);
|
||||
postRequest.setEntity(parametersEntity);
|
||||
addAuthorizationHeader(postRequest);
|
||||
logger.fine(()->"Searching for "+params+"...");
|
||||
final HttpResponse processorResponse = client.execute(postRequest);
|
||||
final InputStream content = processorResponse.getEntity().getContent();
|
||||
final DocumentBuilder builder = XmlUtil.getSecureDocumentBuilderFactory().newDocumentBuilder();
|
||||
final Document document = builder.parse(content);
|
||||
final NodeList dataNodes = document.getElementsByTagName(ROOT_ELEMENT).item(0).getChildNodes();
|
||||
for (int dataNodeIndex=0; dataNodeIndex<dataNodes.getLength(); dataNodeIndex++) {
|
||||
final Node dataNode = dataNodes.item(dataNodeIndex);
|
||||
if (dataNode.getNodeName().equals(DATA_ELEMENT)) {
|
||||
final NodeList rowNodes = dataNode.getChildNodes();
|
||||
for (int rowNodeIndex=0; rowNodeIndex<rowNodes.getLength(); rowNodeIndex++) {
|
||||
final Node rowNode = rowNodes.item(rowNodeIndex);
|
||||
if (rowNode.getNodeName().equals(ROW_ELEMENT)) {
|
||||
final CertificateHandle certificateHandle = parseHandle(rowNode);
|
||||
// certificates can only be obtained if they are currently valid, so ignore expired ones
|
||||
if (includeInvalid || certificateHandle.isValid()) {
|
||||
result.add(certificateHandle);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the JSON {@code Family} string (e.g. {@code "ORC"}) to a {@link CertificateFamily} by matching it
|
||||
* case-insensitively against each family's {@link CertificateFamily#getFamilyQueryParamValue()}. Defaults to
|
||||
* {@link CertificateFamily#ORC} when {@code familyString} is {@code null} or unrecognized, because the JSON actions
|
||||
* only return standard ORC certificates today.
|
||||
*/
|
||||
private static CertificateFamily familyFromString(final String familyString) {
|
||||
CertificateFamily result = CertificateFamily.ORC;
|
||||
if (familyString != null) {
|
||||
for (final CertificateFamily candidate : CertificateFamily.values()) {
|
||||
if (candidate.getFamilyQueryParamValue().equalsIgnoreCase(familyString)) {
|
||||
result = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.fine(()->"Search for "+params+" returned "+result.size()+" results");
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<CountryOverview> getCountriesWithValidCertificates()
|
||||
throws SAXException, IOException, ParserConfigurationException, DOMException, ParseException {
|
||||
final Set<ORCPublicCertificateDatabase.CountryOverview> result = new HashSet<>();
|
||||
final HttpClient client = HttpClientBuilder.create().setRedirectStrategy(new LaxRedirectStrategyForAllRedirectResponseCodes()).build();
|
||||
final List<NameValuePair> params = new ArrayList<>();
|
||||
final HttpGet getRequest = new HttpGet(COUNTRY_OVERVIEW_URL);
|
||||
addAuthorizationHeader(getRequest);
|
||||
logger.fine(()->"Searching for "+params+"...");
|
||||
final HttpResponse processorResponse = client.execute(getRequest);
|
||||
final InputStream content = processorResponse.getEntity().getContent();
|
||||
final DocumentBuilder builder = XmlUtil.getSecureDocumentBuilderFactory().newDocumentBuilder();
|
||||
final Document document = builder.parse(content);
|
||||
final NodeList dataNodes = document.getElementsByTagName(ROOT_ELEMENT).item(0).getChildNodes();
|
||||
for (int dataNodeIndex=0; dataNodeIndex<dataNodes.getLength(); dataNodeIndex++) {
|
||||
final Node dataNode = dataNodes.item(dataNodeIndex);
|
||||
if (dataNode.getNodeName().equals(DATA_ELEMENT)) {
|
||||
final NodeList rowNodes = dataNode.getChildNodes();
|
||||
for (int rowNodeIndex=0; rowNodeIndex<rowNodes.getLength(); rowNodeIndex++) {
|
||||
final Node rowNode = rowNodes.item(rowNodeIndex);
|
||||
if (rowNode.getNodeName().equals(ROW_ELEMENT)) {
|
||||
final CountryOverview countryOverview = parseCountryOverview(rowNode);
|
||||
if (countryOverview.getVPPYear() != null) {
|
||||
result.add(countryOverview);
|
||||
}
|
||||
}
|
||||
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) throws Exception {
|
||||
// includeInvalid is retained for API compatibility; the JSON actions only ever return valid certificates, so
|
||||
// every handle produced here is valid and the flag has no effect.
|
||||
final Set<ORCPublicCertificateDatabase.CertificateHandle> result = new HashSet<>();
|
||||
final String trimmedReferenceNumber = referenceNumber == null ? null : referenceNumber.trim();
|
||||
if (trimmedReferenceNumber != null && !trimmedReferenceNumber.isEmpty()) {
|
||||
final JSONObject root = httpGetJson(SINGLE_CERTIFICATE_DOWNLOAD_URL+"&"+EXT_JSON+"&"+KEY_REF_NO+"="+trimmedReferenceNumber);
|
||||
addMatchingHandles(root, result, yachtName, sailNumber, boatClassName, yearOfIssuance);
|
||||
} else if (issuingCountry != null) {
|
||||
final JSONObject root = httpGetJson(BULK_CERTIFICATES_DOWNLOAD_URL+"&"+COUNTRY_ID_KEY+"="+issuingCountry.getThreeLetterIOCCode()+"&"+EXT_JSON);
|
||||
addMatchingHandles(root, result, yachtName, sailNumber, boatClassName, yearOfIssuance);
|
||||
} else {
|
||||
logger.warning("ORC JSON search requires either a reference number or an issuing country; neither was "+
|
||||
"given, returning no results (yachtName="+yachtName+", sailNumber="+sailNumber+", boatClassName="+
|
||||
boatClassName+")");
|
||||
}
|
||||
logger.fine(()->"ORC search returned "+result.size()+" results");
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addMatchingHandles(final JSONObject root, final Set<CertificateHandle> result, final String yachtName,
|
||||
final String sailNumber, final String boatClassName, final Integer yearOfIssuance) {
|
||||
final JSONArray rows = (JSONArray) root.get(RMS_ARRAY_KEY);
|
||||
if (rows != null) {
|
||||
for (final Object rowObject : rows) {
|
||||
final CertificateHandle handle = parseHandleFromJson((JSONObject) rowObject);
|
||||
if (matchesClientFilters(handle, yachtName, sailNumber, boatClassName, yearOfIssuance)) {
|
||||
result.add(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.fine(()->"Search for "+params+" returned "+result.size()+" results");
|
||||
return result;
|
||||
}
|
||||
|
||||
private CountryOverview parseCountryOverview(Node rowNode) throws DOMException, ParseException {
|
||||
final DateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.mmmX");
|
||||
CountryCode issuingCountry = null;
|
||||
CertificateFamily family = null;
|
||||
Integer certType = null;
|
||||
Integer vppYear = null;
|
||||
int certCount = 0;
|
||||
TimePoint lastUpdate = null;
|
||||
String certName = null;
|
||||
String rmsCode = null;
|
||||
for (int i=0; i<rowNode.getChildNodes().getLength(); i++) {
|
||||
final Node child = rowNode.getChildNodes().item(i);
|
||||
switch (child.getNodeName()) {
|
||||
case "CountryId":
|
||||
issuingCountry = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(child.getTextContent().trim());
|
||||
break;
|
||||
case "Family":
|
||||
family = Util.hasLength(child.getTextContent().trim()) ? CertificateFamily.fromId(Integer.valueOf(child.getTextContent().trim())) : null;
|
||||
break;
|
||||
case "CertType":
|
||||
certType = child.getTextContent().trim().isEmpty() ? null : Integer.valueOf(child.getTextContent().trim());
|
||||
break;
|
||||
case "VPPYear":
|
||||
vppYear = child.getTextContent().trim().isEmpty() ? null : Integer.valueOf(child.getTextContent().trim());
|
||||
break;
|
||||
case "CertCount":
|
||||
certCount = Integer.valueOf(child.getTextContent().trim()).intValue();
|
||||
break;
|
||||
case "LastUpdate":
|
||||
lastUpdate = TimePoint.of(isoDateFormat.parse(child.getTextContent().trim()));
|
||||
break;
|
||||
case "CertName":
|
||||
certName = child.getTextContent().trim();
|
||||
break;
|
||||
case "RMSCode":
|
||||
rmsCode = child.getTextContent().trim();
|
||||
break;
|
||||
|
||||
/**
|
||||
* Applies the optional client-side restrictions to a {@link CertificateHandle} obtained from the JSON actions. A
|
||||
* {@code null} criterion imposes no restriction. The {@code sailNumber} comparison normalizes both sides to their
|
||||
* alphanumeric core (uppercased) so that {@code "DEN 13"}, {@code "DEN-13"}, {@code "DEN13"} and the
|
||||
* {@code %}-bearing fuzzy variants all collapse to a comparable form. The {@code yearOfIssuance} is compared
|
||||
* against the year of the handle's {@link CertificateHandle#getIssueDate() issue date}; note that the JSON actions
|
||||
* only return currently valid certificates, so a historical year effectively matches nothing and year search is
|
||||
* limited to the current issuance year.
|
||||
*/
|
||||
private static boolean matchesClientFilters(final CertificateHandle handle, final String yachtName,
|
||||
final String sailNumber, final String boatClassName, final Integer yearOfIssuance) {
|
||||
boolean match = true;
|
||||
if (yachtName != null) {
|
||||
final String normalizedFilter = normalizeContains(yachtName);
|
||||
match = match && handle.getYachtName() != null
|
||||
&& handle.getYachtName().toLowerCase().contains(normalizedFilter);
|
||||
}
|
||||
if (boatClassName != null) {
|
||||
final String normalizedFilter = normalizeContains(boatClassName);
|
||||
match = match && handle.getBoatClassName() != null
|
||||
&& handle.getBoatClassName().toLowerCase().contains(normalizedFilter);
|
||||
}
|
||||
if (sailNumber != null) {
|
||||
final String normalizedFilter = normalizeSailNumber(sailNumber);
|
||||
match = match && handle.getSailNumber() != null
|
||||
&& normalizeSailNumber(handle.getSailNumber()).contains(normalizedFilter);
|
||||
}
|
||||
if (yearOfIssuance != null) {
|
||||
match = match && handle.getIssueDate() != null
|
||||
&& Instant.ofEpochMilli(handle.getIssueDate().asMillis()).atOffset(ZoneOffset.UTC)
|
||||
.getYear() == yearOfIssuance.intValue();
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
private static String normalizeContains(final String value) {
|
||||
return value.replace("%", "").toLowerCase().trim();
|
||||
}
|
||||
|
||||
private static String normalizeSailNumber(final String sailNumber) {
|
||||
return sailNumber.replaceAll("[^A-Za-z0-9]", "").toUpperCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<CountryOverview> getCountriesWithValidCertificates() throws IOException {
|
||||
// Read the country list from the JSON DownRMS action's top-level "Countries" array (the legacy /RMS XML action
|
||||
// remains available as a fallback). The JSON does not carry a VPP year or certificate counts, so those are left
|
||||
// unset; the first pseudo-entry "ORC" ("Offshore Racing Congress") is skipped.
|
||||
final Set<ORCPublicCertificateDatabase.CountryOverview> result = new HashSet<>();
|
||||
final JSONObject root = httpGetJson(BULK_CERTIFICATES_DOWNLOAD_URL+"&"+EXT_JSON);
|
||||
final JSONArray countries = (JSONArray) root.get(COUNTRIES_ARRAY_KEY);
|
||||
if (countries != null) {
|
||||
for (final Object countryObject : countries) {
|
||||
final JSONObject country = (JSONObject) countryObject;
|
||||
final String countryId = asTrimmedString(country.get(COUNTRY_ID_KEY));
|
||||
final String countryName = asString(country.get(COUNTRY_NAME_KEY));
|
||||
if (countryId != null && !ORC_PSEUDO_COUNTRY_ID.equalsIgnoreCase(countryId)) {
|
||||
final CountryCode issuingCountry = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(countryId);
|
||||
result.add(new CountryOverviewImpl(issuingCountry, CertificateFamily.ORC, /* certType */ null,
|
||||
/* vppYear */ null, /* certCount */ 0, /* lastUpdate */ null, countryName, countryId));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new CountryOverviewImpl(issuingCountry, family, certType, vppYear, certCount, lastUpdate, certName, rmsCode);
|
||||
logger.fine(()->"ORC country overview returned "+result.size()+" countries");
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addAuthorizationHeader(final HttpMessage httpMessage) {
|
||||
httpMessage.addHeader("Authorization", "Basic "+new String(Base64.getEncoder().encode((USER_NAME+":"+getDecodedCredentials()).getBytes())));
|
||||
}
|
||||
|
||||
private CertificateHandle parseHandle(Node rowNode) throws DOMException, ParseException {
|
||||
assert rowNode.getNodeName().equals(ROW_ELEMENT);
|
||||
CountryCode issuingCountry = null;
|
||||
String fileId = null;
|
||||
String sssid = null;
|
||||
Double gph = null;
|
||||
UUID datInGID = null;
|
||||
String referenceNumber = null;
|
||||
CertificateFamily family = CertificateFamily.UNKNOWN;
|
||||
String yachtName = null;
|
||||
String sailNumber = null;
|
||||
String boatClassName = null;
|
||||
String designer = null;
|
||||
String builder = null;
|
||||
Integer yearBuilt = null;
|
||||
Integer certType = null;
|
||||
TimePoint issueDate = null;
|
||||
Boolean isOneDesign = null;
|
||||
Boolean isProvisional = null;
|
||||
Boolean isValid = null;
|
||||
for (int i=0; i<rowNode.getChildNodes().getLength(); i++) {
|
||||
final Node child = rowNode.getChildNodes().item(i);
|
||||
switch (child.getNodeName()) {
|
||||
case "CountryId":
|
||||
issuingCountry = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(child.getTextContent().trim());
|
||||
break;
|
||||
case "SSSID":
|
||||
sssid = child.getTextContent();
|
||||
break;
|
||||
case "GPH":
|
||||
gph = child.getTextContent().trim().isEmpty() ? null : Double.valueOf(child.getTextContent().trim());
|
||||
break;
|
||||
case "DatInGID":
|
||||
datInGID = UUID.fromString(child.getTextContent().trim().replaceAll("[{}]", ""));
|
||||
break;
|
||||
case "RefNo":
|
||||
referenceNumber = child.getTextContent().trim();
|
||||
break;
|
||||
case "Family":
|
||||
family = child.getTextContent().trim().isEmpty() ? CertificateFamily.UNKNOWN : CertificateFamily.fromId(Integer.valueOf(child.getTextContent().trim()));
|
||||
break;
|
||||
case "YachtName":
|
||||
yachtName = child.getTextContent();
|
||||
break;
|
||||
case "SailNo":
|
||||
sailNumber = child.getTextContent();
|
||||
break;
|
||||
case "Class":
|
||||
boatClassName = child.getTextContent();
|
||||
break;
|
||||
case "Designer":
|
||||
designer = child.getTextContent();
|
||||
break;
|
||||
case "Builder":
|
||||
builder = child.getTextContent();
|
||||
break;
|
||||
case "dxtDate":
|
||||
issueDate = new MillisecondsTimePoint(parseDate(child.getTextContent())); // assume UTC
|
||||
break;
|
||||
case "dxtName":
|
||||
final String fileIdWithOptionalDxtSuffix = child.getTextContent();
|
||||
fileId = fileIdWithOptionalDxtSuffix.toLowerCase().endsWith(DXT_SUFFIX) ?
|
||||
fileIdWithOptionalDxtSuffix.substring(0, fileIdWithOptionalDxtSuffix.length()-DXT_SUFFIX.length()) :
|
||||
fileIdWithOptionalDxtSuffix;
|
||||
break;
|
||||
case "CertType":
|
||||
certType = Integer.valueOf(child.getTextContent().trim());
|
||||
break;
|
||||
case "Age":
|
||||
yearBuilt = child.getTextContent().trim().isEmpty() ? null : Integer.valueOf(child.getTextContent().trim());
|
||||
break;
|
||||
case "IsOd":
|
||||
isOneDesign = Boolean.valueOf(child.getTextContent());
|
||||
break;
|
||||
case "Provisional":
|
||||
isProvisional = Boolean.valueOf(child.getTextContent());
|
||||
break;
|
||||
case "CanSelect":
|
||||
isValid = Boolean.valueOf(child.getTextContent());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new CertificateHandleImpl(issuingCountry, fileId, gph, sssid, datInGID, referenceNumber, family==null?CertificateFamily.UNKNOWN:family,
|
||||
yachtName, sailNumber, boatClassName, designer, builder, yearBuilt, issueDate, certType, isOneDesign,
|
||||
isProvisional, isValid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date parseDate(final String dateString) throws DateTimeParseException {
|
||||
Optional<LocalDateTime> parsedDateWithoutZone = Arrays.asList(DATE_FORMATTERS).stream()
|
||||
@@ -591,21 +596,19 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
|
||||
@Override
|
||||
public ORCCertificate searchForUpdate(CertificateHandle certificateHandle)
|
||||
throws ClientProtocolException, IOException, org.json.simple.parser.ParseException {
|
||||
return searchForUpdate(certificateHandle.getIssuingCountry(), certificateHandle.getFileId(), certificateHandle.getReferenceNumber());
|
||||
return searchForUpdate(certificateHandle.getReferenceNumber());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ORCCertificate searchForUpdate(ORCCertificate certificate)
|
||||
throws ClientProtocolException, IOException, org.json.simple.parser.ParseException {
|
||||
return searchForUpdate(certificate.getIssuingCountry(), certificate.getFileId(), certificate.getReferenceNumber());
|
||||
return searchForUpdate(certificate.getReferenceNumber());
|
||||
}
|
||||
|
||||
private ORCCertificate searchForUpdate(final CountryCode issuingCountry, final String fileId,
|
||||
final String existingCertificateRefNo)
|
||||
private ORCCertificate searchForUpdate(final String existingCertificateRefNo)
|
||||
throws IOException, org.json.simple.parser.ParseException, ClientProtocolException {
|
||||
logger.fine("Looking for update of certificate with reference number "+existingCertificateRefNo);
|
||||
final String queryParameters = "ext=json&CountryId=" + issuingCountry.getThreeLetterIOCCode()+
|
||||
"&dxtName="+fileId+".dxt";
|
||||
final String queryParameters = EXT_JSON+"&"+KEY_REF_NO+"="+existingCertificateRefNo;
|
||||
final ORCCertificate result = getSingleCertificate(queryParameters);
|
||||
if (result == null) {
|
||||
logger.info("Couldn't find any valid certificate for that boat");
|
||||
@@ -733,7 +736,10 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
|
||||
|
||||
/**
|
||||
* Varies the boat class name by first using the true boat class name, then, if nothing is found, stepping through
|
||||
* the alternative names, and finally removing the boat class name constraint altogether.
|
||||
* the alternative names, and finally removing the boat class name constraint altogether. Because the JSON bulk
|
||||
* action needs an issuing country to return rows, the nationality is derived from the {@code sailNumber} prefix via
|
||||
* {@link #countryFromSailNumber(String)}; a sail number without a resolvable three-letter IOC prefix will therefore
|
||||
* yield no results.
|
||||
*
|
||||
* @param yachtName
|
||||
* may be {@code null}
|
||||
@@ -746,14 +752,15 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
|
||||
* sequence of handles; may be {@code null}
|
||||
*/
|
||||
private Iterable<CertificateHandle> fuzzySearchVaryingBoatClassName(final String yachtName, final String sailNumber, final BoatClass boatClass) throws Exception {
|
||||
final CountryCode issuingCountry = countryFromSailNumber(sailNumber);
|
||||
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, /* yearOfIssuance */ null, /* referenceNumber */ null, yachtName, sailNumber, successfulBoatClassName, /* includeInvalid */ false);
|
||||
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, /* yearOfIssuance */ null, /* referenceNumber */ null, yachtName, sailNumber, successfulBoatClassName, /* includeInvalid */ false);
|
||||
} else {
|
||||
logger.fine(()->"No current certificates found for boat class "+boatClass.getName()+
|
||||
" but yacht name and sail number are not specified either; giving up.");
|
||||
@@ -775,6 +782,26 @@ public class ORCPublicCertificateDatabaseImpl implements ORCPublicCertificateDat
|
||||
return certificateHandles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the leading three-letter nationality prefix from a sail number (e.g. {@code "GER"} from
|
||||
* {@code "GER 5711"}) and resolves it to a {@link CountryCode} via its IOC code, mirroring the prefix extraction in
|
||||
* {@link #getSailNumberVariants(String)}. Returns {@code null} when {@code sailNumber} is {@code null} or has no
|
||||
* resolvable three-letter prefix.
|
||||
*/
|
||||
private static CountryCode countryFromSailNumber(final String sailNumber) {
|
||||
CountryCode result = null;
|
||||
if (sailNumber != null) {
|
||||
final Matcher matcher = Pattern.compile("[^A-Za-z]*([A-Za-z]*).*([0-9]*).*").matcher(sailNumber);
|
||||
if (matcher.find()) {
|
||||
final String country = matcher.group(1);
|
||||
if (country != null && country.length() == 3) {
|
||||
result = CountryCodeFactory.INSTANCE.getFromThreeLetterIOCName(country.toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a sequence of sail number variations of which {@code sailNumber} is the first.
|
||||
* It then adds separations of a recognized three-letter IOC nationality code at the beginning
|
||||
|
||||
Reference in new issue
Block a user