From d6dbf7a64d73918468ebab63b4677fdb63c3341e Mon Sep 17 00:00:00 2001 From: haphut Date: Thu, 18 Dec 2025 15:34:54 +0200 Subject: [PATCH 1/7] build: Update to Java 25 --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 782a860..032d2ab 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ UTF-8 - 11 - 11 + 25 + 25 2.0.7 2.43.0 1.17.0 @@ -88,8 +88,8 @@ maven-compiler-plugin 3.11.0 - 11 - 11 + 25 + 25 From 8c7ae6a4abc5cfdf0ff3eeb508d78d636d3d791b Mon Sep 17 00:00:00 2001 From: haphut Date: Thu, 18 Dec 2025 19:29:28 +0200 Subject: [PATCH 2/7] ci: Switch to transitdata-shared-workflows --- .github/workflows/ci-cd.yml | 12 ++++ .github/workflows/test-and-build.yml | 85 ---------------------------- 2 files changed, 12 insertions(+), 85 deletions(-) create mode 100644 .github/workflows/ci-cd.yml delete mode 100644 .github/workflows/test-and-build.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..80f6347 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,12 @@ +name: CI/CD + +on: + - pull_request + - push + +jobs: + build: + uses: HSLdevcom/transitdata-shared-workflows/.github/workflows/ci-cd-java.yml@1.0.0 + secrets: + DOCKER_USERNAME: ${{ secrets.TRANSITDATA_DOCKERHUB_USER }} + DOCKER_PASSWORD: ${{ secrets.TRANSITDATA_DOCKERHUB_TOKEN }} diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml deleted file mode 100644 index d08acc8..0000000 --- a/.github/workflows/test-and-build.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Test and create Docker image - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '11' - cache: 'maven' - - name: Run Spotless Apply - run: mvn spotless:apply - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Build with Maven - run: mvn --file pom.xml clean install - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Upload .jar file - uses: actions/upload-artifact@v4 - with: - name: transitdata-metro-ats-parser-jar-with-dependencies.jar - path: target/transitdata-metro-ats-parser-jar-with-dependencies.jar - build-develop-docker-image: - needs: test - runs-on: ubuntu-latest - # Run only on develop branch - if: github.ref == 'refs/heads/develop' - steps: - - uses: actions/checkout@v3 - - name: Download .jar file - uses: actions/download-artifact@v4 - with: - name: transitdata-metro-ats-parser-jar-with-dependencies.jar - path: target - - name: Build and publish develop Docker image - uses: elgohr/Publish-Docker-Github-Action@v5 - with: - name: hsldevcom/transitdata-metro-ats-parser - username: ${{ secrets.TRANSITDATA_DOCKERHUB_USER }} - password: ${{ secrets.TRANSITDATA_DOCKERHUB_TOKEN }} - tags: develop - build-release-docker-image: - needs: test - runs-on: ubuntu-latest - # Run only for tagged commits - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - steps: - - uses: actions/checkout@v3 - - name: Download .jar file - uses: actions/download-artifact@v4 - with: - name: transitdata-metro-ats-parser-jar-with-dependencies.jar - path: target - - name: Build and publish release Docker image - uses: elgohr/Publish-Docker-Github-Action@v5 - with: - name: hsldevcom/transitdata-metro-ats-parser - username: ${{ secrets.TRANSITDATA_DOCKERHUB_USER }} - password: ${{ secrets.TRANSITDATA_DOCKERHUB_TOKEN }} - tag_semver: true - build-aks-dev-docker-image: - needs: test - runs-on: ubuntu-latest - # Run only on aks-dev branch - if: github.ref == 'refs/heads/aks-dev' - steps: - - uses: actions/checkout@v2 - - name: Download .jar file - uses: actions/download-artifact@v4 - with: - name: transitdata-metro-ats-parser-jar-with-dependencies.jar - path: target - - name: Build and publish aks-dev Docker image - uses: elgohr/Publish-Docker-Github-Action@master - with: - name: hsldevcom/transitdata-metro-ats-parser - username: ${{ secrets.TRANSITDATA_DOCKERHUB_USER }} - password: ${{ secrets.TRANSITDATA_DOCKERHUB_TOKEN }} - tags: aks-dev \ No newline at end of file From 3bee823b99da70b203846c05d2f212d23fb72c77 Mon Sep 17 00:00:00 2001 From: haphut Date: Thu, 18 Dec 2025 17:21:24 +0200 Subject: [PATCH 3/7] build: Use infodevops-docker-base-images:1.0.0-java-25 --- Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index a4aa406..d55d935 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,7 @@ -FROM eclipse-temurin:11-alpine -#Install curl for health check -RUN apk add --no-cache curl +FROM hsldevcom/infodevops-docker-base-images:1.0.0-java-25 ADD target/transitdata-metro-ats-parser-jar-with-dependencies.jar /usr/app/transitdata-metro-ats-parser.jar COPY start-application.sh / RUN chmod +x /start-application.sh -CMD ["/start-application.sh"] \ No newline at end of file +CMD ["/start-application.sh"] From f5d221ff6e86794b08cc539df1871209a20df8b7 Mon Sep 17 00:00:00 2001 From: haphut Date: Thu, 18 Dec 2025 14:02:05 +0200 Subject: [PATCH 4/7] test: Test filtering before first departure --- .../MetroEstimatesFactoryTest.java | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/test/java/fi.hsl.transitdata.metroats/MetroEstimatesFactoryTest.java b/src/test/java/fi.hsl.transitdata.metroats/MetroEstimatesFactoryTest.java index ecd05d8..d2f6df7 100644 --- a/src/test/java/fi.hsl.transitdata.metroats/MetroEstimatesFactoryTest.java +++ b/src/test/java/fi.hsl.transitdata.metroats/MetroEstimatesFactoryTest.java @@ -1,15 +1,14 @@ package fi.hsl.transitdata.metroats; +import static org.junit.Assert.*; + import fi.hsl.common.files.FileUtils; import fi.hsl.transitdata.metroats.models.MetroEstimate; import fi.hsl.transitdata.metroats.models.MetroStopEstimate; -import org.junit.Test; - import java.io.InputStream; import java.net.URL; import java.util.Optional; - -import static org.junit.Assert.*; +import org.junit.Test; public class MetroEstimatesFactoryTest { @@ -32,4 +31,40 @@ public void testDateTimeConversion() throws Exception { assertEquals("2019-07-09T05:06:13.941Z", metroStopEstimate.departureTimeForecast); assertEquals("2019-07-09T05:06:32.578Z", metroStopEstimate.departureTimeMeasured); } + + @Test + public void testParsePayloadWithNoMeasuredDepartureTimeForFirstStation() throws Exception { + // The API surprisingly uses "null" instead of null in JSON. + String json = """ + { + "routeName": "M1", + "beginTime": "2023-01-01T12:01:02.345Z", + "routeRows": [ + { + "station": "KIV", + "departureTimeMeasured": "null" + } + ] + }"""; + Optional result = MetroEstimatesFactory.parsePayload(json.getBytes()); + assertFalse("Should filter out messages without a measured departure time for first station", + result.isPresent()); + } + + @Test + public void testParsePayloadWithMeasuredDepartureTimeForFirstStation() throws Exception { + String json = """ + { + "routeName": "M1", + "beginTime": "2023-01-01T12:01:02.345Z", + "routeRows": [ + { + "station": "KIV", + "departureTimeMeasured": "2023-01-01T12:01:05.678Z" + } + ] + }"""; + Optional result = MetroEstimatesFactory.parsePayload(json.getBytes()); + assertTrue("Should accept messages with a measured departure time for first station", result.isPresent()); + } } From cb17b7e0cfc412f66f9f06ea31ca79b2a398a20d Mon Sep 17 00:00:00 2001 From: haphut Date: Thu, 18 Dec 2025 15:17:46 +0200 Subject: [PATCH 5/7] feat: Filter out forecasts before first departure --- .../metroats/MetroEstimatesFactory.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java b/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java index 594bf4f..e06c62e 100644 --- a/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java +++ b/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java @@ -368,9 +368,35 @@ private Optional> getMetroJourneyData(final String metroKey) } } + private static boolean hasFirstRowMeasuredDeparture(MetroEstimate estimate) { + if (estimate.routeRows != null && !estimate.routeRows.isEmpty()) { + return estimate.routeRows.get(0).departureTimeMeasured != null; + } + return true; + } + public static Optional parsePayload(final byte[] payload) { try { MetroEstimate metroEstimate = mapper.readValue(payload, MetroEstimate.class); + + /** + * We have learned that the Metro Mipro ATS API sends unusable + * forecasts for a vehicle journey for a few minutes before the + * departure from the first station. Those faulty forecasts tend to + * forecast an early departure from the first station which almost + * never happens as the drivers will wait until the vehicle journey + * is planned to start. + * + * The workaround is to throw away forecasts before the vehicle has + * departed from the first station. + */ + if (!hasFirstRowMeasuredDeparture(metroEstimate)) { + log.debug( + "Dropped untrustworthy Mipro ATS forecast that was given before departure from first station. Payload: {}", + new String(payload)); + return Optional.empty(); + } + return Optional.of(metroEstimate); } catch (Exception e) { log.warn("Failed to parse payload {}.", new String(payload), e); From ff1ee20ecc1d4b3978b186b5d4c5c79c78bdeb90 Mon Sep 17 00:00:00 2001 From: haphut Date: Mon, 22 Dec 2025 00:33:06 +0200 Subject: [PATCH 6/7] refactor: Prettify Boolean logic --- .../fi/hsl/transitdata/metroats/MetroEstimatesFactory.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java b/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java index e06c62e..2c765a9 100644 --- a/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java +++ b/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java @@ -369,10 +369,8 @@ private Optional> getMetroJourneyData(final String metroKey) } private static boolean hasFirstRowMeasuredDeparture(MetroEstimate estimate) { - if (estimate.routeRows != null && !estimate.routeRows.isEmpty()) { - return estimate.routeRows.get(0).departureTimeMeasured != null; - } - return true; + return estimate.routeRows == null || estimate.routeRows.isEmpty() + || estimate.routeRows.get(0).departureTimeMeasured != null; } public static Optional parsePayload(final byte[] payload) { From 42c0aa7035dc3d39f200c79af46a5cb21e47a817 Mon Sep 17 00:00:00 2001 From: haphut Date: Mon, 22 Dec 2025 00:38:36 +0200 Subject: [PATCH 7/7] style: Use semantic naming --- .../metroats/MetroEstimatesFactory.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java b/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java index 2c765a9..4d9244f 100644 --- a/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java +++ b/src/main/java/fi/hsl/transitdata/metroats/MetroEstimatesFactory.java @@ -368,7 +368,21 @@ private Optional> getMetroJourneyData(final String metroKey) } } - private static boolean hasFirstRowMeasuredDeparture(MetroEstimate estimate) { + /** + * Checks if the metro prediction is considered fairly reliable. + * + * We have learned that the Metro Mipro ATS API sends unreliable predictions for a vehicle journey for a few + * minutes before the departure from the first station. Those faulty predictions tend to predict an early departure + * from the first station. That almost never happens as the drivers will wait until the vehicle journey is planned + * to start. + * + * This method filters out predictions where the first station's departureTimeMeasured is missing (or invalid). + * + * @param estimate the MetroEstimate to validate + * @return true if the predictions can be considered fairly reliable or no predictions were given, + * false if the predictions cannot be considered fairly reliable + */ + private static boolean arePredictionsFairlyReliable(MetroEstimate estimate) { return estimate.routeRows == null || estimate.routeRows.isEmpty() || estimate.routeRows.get(0).departureTimeMeasured != null; } @@ -376,25 +390,12 @@ private static boolean hasFirstRowMeasuredDeparture(MetroEstimate estimate) { public static Optional parsePayload(final byte[] payload) { try { MetroEstimate metroEstimate = mapper.readValue(payload, MetroEstimate.class); - - /** - * We have learned that the Metro Mipro ATS API sends unusable - * forecasts for a vehicle journey for a few minutes before the - * departure from the first station. Those faulty forecasts tend to - * forecast an early departure from the first station which almost - * never happens as the drivers will wait until the vehicle journey - * is planned to start. - * - * The workaround is to throw away forecasts before the vehicle has - * departed from the first station. - */ - if (!hasFirstRowMeasuredDeparture(metroEstimate)) { + if (!arePredictionsFairlyReliable(metroEstimate)) { log.debug( - "Dropped untrustworthy Mipro ATS forecast that was given before departure from first station. Payload: {}", + "Dropped untrustworthy Mipro ATS predictions that were given before departure from first station. Payload: {}", new String(payload)); return Optional.empty(); } - return Optional.of(metroEstimate); } catch (Exception e) { log.warn("Failed to parse payload {}.", new String(payload), e);