From 45fa0c1791bf78597d3edcccc333eb47d1b0ea1c Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 22 Apr 2026 11:57:45 +0200 Subject: [PATCH 1/9] Update GOPACSAddressBookResource to UFTP Participants v3 endpoint Replaces the deprecated v2 list-by-EAN endpoint with the v3 lookup-by-domain endpoint (GET /uftp-participants/v3/participants/{uftpDomainName}). The response is now JSON with an Authorization header carrying a Bearer token. --- .../gopacs/GOPACSAddressBookResource.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java index 6ed4a0d..a119a08 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSAddressBookResource.java @@ -19,17 +19,22 @@ */ package org.openremote.extension.ems.manager.gopacs; -import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; -import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; -import static jakarta.ws.rs.core.MediaType.APPLICATION_XML; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -@Path("v2/participants/DSO") +@Path("uftp-participants/v3/participants") public interface GOPACSAddressBookResource { @GET - @Consumes(APPLICATION_XML) - Response fetchParticipants(@QueryParam("contractedEan") String contractedEan); + @Path("{uftpDomainName}") + @Produces(APPLICATION_JSON) + Response fetchParticipantByDomain( + @HeaderParam("Authorization") String authorization, + @PathParam("uftpDomainName") String uftpDomainName + ); } From b698a0a5098ee350c5fe1cf998046b34fe646444 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 22 Apr 2026 11:58:10 +0200 Subject: [PATCH 2/9] Add ParticipantView DTO mirroring the Participants v3 response schema --- .../ems/manager/gopacs/ParticipantView.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java new file mode 100644 index 0000000..828b8c6 --- /dev/null +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java @@ -0,0 +1,25 @@ +/* + * Copyright 2025, OpenRemote Inc. + * + * See the CONTRIBUTORS.txt file in the distribution for a + * full listing of individual contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.openremote.extension.ems.manager.gopacs; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record ParticipantView(String domain, String publicKey) {} From c154fa947a11d64f26b044858649753d1e425ffb Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 22 Apr 2026 11:59:40 +0200 Subject: [PATCH 3/9] Switch GOPACSHandler participant lookup to UFTP Participants v3 - Default base URL changes from clc-message-broker.gopacs-services.eu to api.gopacs-services.eu (override via GOPACS_PARTICIPANT_URL, e.g. api.acc.gopacs-services.eu for acceptance). - getParticipantInformation now calls the v3 by-domain endpoint with a Bearer token instead of listing all participants for an EAN. Adds 404 handling for unknown domains; other non-2xx statuses log at SEVERE. - Extract fetchBearerToken() so Shapeshifter's getAuthorizationHeader and the participant lookup share one OAuth2 flow. - v3 ParticipantView has no endpoint field so the cached UftpParticipantInformation is constructed with endpoint=null. --- .../ems/manager/gopacs/GOPACSHandler.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java index 5443d56..bed4576 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.GenericType; import jakarta.ws.rs.core.Response; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.lfenergy.shapeshifter.api.*; @@ -89,7 +88,7 @@ public class GOPACSHandler implements UftpPayloadHandler, UftpParticipantService private static final Logger LOG = SyslogCategory.getLogger(API, GOPACSHandler.class); public static final String GOPACS_PRIVATE_KEY_FILE = "GOPACS_PRIVATE_KEY_FILE"; public static final String GOPACS_PARTICIPANT_URL = "GOPACS_PARTICIPANT_URL"; - public static final String DEFAULT_GOPACS_PARTICIPANT_URL = "https://clc-message-broker.gopacs-services.eu"; + public static final String DEFAULT_GOPACS_PARTICIPANT_URL = "https://api.gopacs-services.eu"; public static final String GOPACS_OAUTH2_URL = "GOPACS_OAUTH2_URL"; public static final String DEFAULT_GOPACS_OAUTH2_URL = "https://auth.gopacs-services.eu/realms/gopacs/protocol/openid-connect/token"; public static final String GOPACS_CLIENT_ID = "GOPACS_CLIENT_ID"; @@ -293,8 +292,11 @@ public void notifyNewOutgoingMessage(OutgoingUftpMessage getParticipantInformation(USEFRoleType role, String domain) { if (participants.containsKey(domain)) { return Optional.of(participants.get(domain)); - } else { - try (Response response = gopacsAddressBookResource.fetchParticipants(contractedEAN)) { - if (response != null && response.getStatus() == 200) { - List participants = response.readEntity(new GenericType<>() { - }); - for (UftpParticipantInformation participant : participants) { - this.participants.put(participant.domain(), new UftpParticipantInformation(participant.domain(), participant.publicKey(), participant.endpoint(), true)); - } - return participants.stream().filter(p -> p.domain().equals(domain)).findFirst(); - } - } catch (Exception e) { - if (e.getCause() != null && e.getCause() instanceof IOException) { - LOG.log(Level.SEVERE, "Exception when requesting participant information", e.getCause()); - } else { - LOG.log(Level.SEVERE, "Exception when requesting participant information", e); - } + } + + String authorization = fetchBearerToken(); + try (Response response = gopacsAddressBookResource.fetchParticipantByDomain(authorization, domain)) { + int status = response != null ? response.getStatus() : -1; + if (status == 200) { + ParticipantView view = response.readEntity(ParticipantView.class); + UftpParticipantInformation info = new UftpParticipantInformation(view.domain(), view.publicKey(), null, true); + participants.put(view.domain(), info); + return Optional.of(info); } + if (status == 404) { + LOG.fine("Participant not found in GOPACS address book: " + domain); + } else { + LOG.severe("Unexpected status " + status + " when requesting participant information for " + domain); + } + } catch (Exception e) { + Throwable cause = e.getCause() instanceof IOException ? e.getCause() : e; + LOG.log(Level.SEVERE, "Exception when requesting participant information for " + domain, cause); } return Optional.empty(); } From a42c6c933522de4ee0cb8130b182187e1f14bda3 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 22 Apr 2026 12:00:21 +0200 Subject: [PATCH 4/9] Add GitHub issue draft for the v2-to-v3 migration --- GOPACS_PARTICIPANTS_V3_ISSUE.md | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 GOPACS_PARTICIPANTS_V3_ISSUE.md diff --git a/GOPACS_PARTICIPANTS_V3_ISSUE.md b/GOPACS_PARTICIPANTS_V3_ISSUE.md new file mode 100644 index 0000000..3879db3 --- /dev/null +++ b/GOPACS_PARTICIPANTS_V3_ISSUE.md @@ -0,0 +1,66 @@ +# Migrate GOPACS UFTP Participants API from V2 to V3 + +**Label:** Enhancement + +## Background + +GOPACS has announced (email, April 2026) that the **UFTP Participants API V2** is deprecated and will be removed starting **26 October 2026**. The acceptance environment is removed first, then production. Our EMS integration in `ems/manager/gopacs/` calls this API whenever Shapeshifter needs a peer's public key to verify an incoming UFTP message signature, so if we miss the deadline, inbound GOPACS messaging stops working. + +The main functional change in V3 is that participants can now be looked up by **ContractID + role** as well as by domain, which lets GOPACS route activation messages to per-contract handlers. We are **not** adopting that new capability in this migration — it would require restructuring `GOPACSHandler` (currently one handler per EAN) and can be a follow-up if we ever need per-Capacity-Steering-Contract / per-Time-bound-Transport-Right behaviour. This migration is a minimal drop-in replacement to stay functional past the deadline. + +## What's changing in V3 + +| | V2 | V3 | +|---|---|---| +| Base URL (prod) | `https://clc-message-broker.gopacs-services.eu` | `https://api.gopacs-services.eu` | +| Base URL (acc) | *n/a via our config* | `https://api.acc.gopacs-services.eu` | +| Participant lookup | `GET /v2/participants/DSO?contractedEan={ean}` → list of all DSOs for an EAN | `GET /uftp-participants/v3/participants/{uftpDomainName}` → single `ParticipantView` | +| Content type | XML | JSON | +| Auth | (not enforced in our client) | Bearer JWT | +| Response fields | `domain`, `publicKey`, `endpoint`, … | `domain`, `publicKey` only | +| Removed | — | An unspecified "old and temporary endpoint" | + +V3 also adds `GET /uftp-participants/v3/participants/contracts/{contractId}/roles/{uftpRole}` (roles: `AGR`, `DSO`; `CRO` listed as not supported). Our signature-verification call site always has the sender domain so the by-domain endpoint is the direct substitute — we don't need the contract/role endpoint for this migration. + +Swagger documentation: +- Acceptance: https://api.acc.gopacs-services.eu/docs/uftp-broker/v3/api-docs/uftp-participants-v3 +- Production: https://api.gopacs-services.eu/docs/uftp-broker/v3/api-docs/uftp-participants-v3 + +## Scope of this change + +- [x] Update `GOPACSAddressBookResource` to the V3 by-domain endpoint (JSON, Authorization header). +- [x] Add `ParticipantView` DTO record mirroring the V3 response schema. +- [x] Rewrite `GOPACSHandler.getParticipantInformation(...)` to call the V3 endpoint with a Bearer token from the existing OAuth2 client-credentials flow. Extract the token acquisition into a shared `fetchBearerToken()` helper. +- [x] Change `DEFAULT_GOPACS_PARTICIPANT_URL` to `https://api.gopacs-services.eu`. Acceptance environment selected via existing `GOPACS_PARTICIPANT_URL` override. +- [x] Drop the `endpoint` field from cached `UftpParticipantInformation` (V3 no longer returns it — we pass `null`). + +Out of scope (follow-up if needed): +- Per-ContractID / per-role handler registration using `GET /participants/contracts/{contractId}/roles/{uftpRole}`. +- WireMock tests for the V3 lookup — `GOPACSHandler` currently has no unit tests at all, and adding test infrastructure is a larger separate piece of work. +- Upgrading `shapeshifter-core` from `3.2.2` to `3.3.0`. Shapeshifter is not involved in the V2/V3 migration (the participants API is a GOPACS-broker-specific REST API, not part of the UFTP protocol). A library bump can be a separate PR if desired. + +## Deployment note + +Any deployment that overrides `GOPACS_PARTICIPANT_URL` (for example, pinning acceptance to `https://clc-message-broker-acc.gopacs-services.eu`) **must be updated** to the new host: + +- Production: `GOPACS_PARTICIPANT_URL=https://api.gopacs-services.eu` (or leave unset to use the default). +- Acceptance: `GOPACS_PARTICIPANT_URL=https://api.acc.gopacs-services.eu`. + +## Acceptance criteria + +- [ ] `./gradlew clean build` passes. +- [ ] Manager deploys cleanly with a GOPACS-enabled `EmsGOPACSAsset`. +- [ ] Against the acceptance environment: an inbound signed UFTP `FlexRequest` from a known DSO triggers a successful V3 participant lookup (200 with `publicKey`) and signature verification passes in `processRawMessage`. Downstream `POWER_*` predicted datapoints are written as before. +- [ ] 404 from the V3 endpoint for an unknown domain produces a FINE-level log, not an error. +- [ ] Works in both acceptance (`api.acc.gopacs-services.eu`) and production (`api.gopacs-services.eu`) via the `GOPACS_PARTICIPANT_URL` override. + +## Open risk to validate during smoke test + +V3's `ParticipantView` no longer carries an `endpoint` field, so the cached `UftpParticipantInformation` stores `endpoint=null`. Shapeshifter's `UftpSendMessageService` reads the endpoint when sending outbound messages. Needs confirmation during the acceptance smoke test that outgoing `FlexOffer` / `FlexOrderResponse` messaging still works with `endpoint=null`; if not, we'll need a follow-up to either introduce a configured broker URL fallback or route outbound messages differently. + +## Timeline + +- **25 October 2026** — last day GOPACS guarantees V2 is available. +- **26 October 2026** — GOPACS begins removing V2, starting with the acceptance environment. + +Aim to have this merged and deployed to the acceptance environment well ahead of 26 October 2026 so we can validate against acceptance before production removal. From f6841ca5eef66b1dedf0510801dcf7542db38a5d Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 22 Apr 2026 15:19:04 +0200 Subject: [PATCH 5/9] Tighten GOPACS v3 issue: outbound send is a known defect, not a risk Verified against shapeshifter-core 3.2.2 and 3.5.0 sources: the outbound send path does new URI(participantInformation.endpoint()) with no null guard, so endpoint=null NPEs every FlexOffer / FlexRequestResponse / FlexOrderResponse. The NPE escapes the send service's try/catch. Also corrects the library version (3.5.0 exists; it does not fix this). --- GOPACS_PARTICIPANTS_V3_ISSUE.md | 35 ++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/GOPACS_PARTICIPANTS_V3_ISSUE.md b/GOPACS_PARTICIPANTS_V3_ISSUE.md index 3879db3..4a47bc8 100644 --- a/GOPACS_PARTICIPANTS_V3_ISSUE.md +++ b/GOPACS_PARTICIPANTS_V3_ISSUE.md @@ -37,7 +37,7 @@ Swagger documentation: Out of scope (follow-up if needed): - Per-ContractID / per-role handler registration using `GET /participants/contracts/{contractId}/roles/{uftpRole}`. - WireMock tests for the V3 lookup — `GOPACSHandler` currently has no unit tests at all, and adding test infrastructure is a larger separate piece of work. -- Upgrading `shapeshifter-core` from `3.2.2` to `3.3.0`. Shapeshifter is not involved in the V2/V3 migration (the participants API is a GOPACS-broker-specific REST API, not part of the UFTP protocol). A library bump can be a separate PR if desired. +- Upgrading `shapeshifter-core` from `3.2.2` to `3.5.0`. Shapeshifter is not involved in the V2/V3 migration itself (the participants API is a GOPACS-broker-specific REST API, not part of the UFTP protocol). Verified against the 3.5.0 sources: `UftpParticipantInformation`, `ParticipantResolutionService`, and `UftpSendMessageService.doSend()` are unchanged vs. 3.2.2 — so the version bump would not fix the outbound-send defect described below either. A library bump can be a separate PR if desired. ## Deployment note @@ -54,9 +54,38 @@ Any deployment that overrides `GOPACS_PARTICIPANT_URL` (for example, pinning acc - [ ] 404 from the V3 endpoint for an unknown domain produces a FINE-level log, not an error. - [ ] Works in both acceptance (`api.acc.gopacs-services.eu`) and production (`api.gopacs-services.eu`) via the `GOPACS_PARTICIPANT_URL` override. -## Open risk to validate during smoke test +## Known follow-up: outbound messaging needs a broker-URL fallback -V3's `ParticipantView` no longer carries an `endpoint` field, so the cached `UftpParticipantInformation` stores `endpoint=null`. Shapeshifter's `UftpSendMessageService` reads the endpoint when sending outbound messages. Needs confirmation during the acceptance smoke test that outgoing `FlexOffer` / `FlexOrderResponse` messaging still works with `endpoint=null`; if not, we'll need a follow-up to either introduce a configured broker URL fallback or route outbound messages differently. +V3's `ParticipantView` no longer carries an `endpoint` field, so the cached `UftpParticipantInformation` stores `endpoint=null`. Reading `shapeshifter-core` 3.2.2 (and confirmed identical in 3.5.0), the outbound send path is: + +1. `GOPACSHandler.notifyNewOutgoingMessage` → `UftpSendMessageService.attemptToSendMessage` +2. `UftpSendMessageService.doSend` (line 124–133 in 3.2.2, 135–144 in 3.5.0): + ```java + UftpParticipantInformation participantInformation = participantService.getParticipantInformation(details.recipient()); + String url = participantInformation.endpoint(); // null after this migration + ... + send(signedXml, url, additionalHeaders, MAX_FOLLOW_REDIRECTS); + ``` +3. `UftpSendMessageService.send(...)`: + ```java + var requestBuilder = HttpRequest.newBuilder().uri(new URI(url)) ... // new URI(null) → NullPointerException + ``` + +The surrounding `try/catch` in `send` catches `URISyntaxException | IllegalArgumentException | IOException | InterruptedException`. It does not catch `NullPointerException`, so the NPE escapes `attemptToSendMessage` and is swallowed only by `GOPACSHandler.notifyNewOutgoingMessage`'s generic `catch (Exception e)` as a SEVERE log. + +**Practical impact:** every outbound `FlexOffer`, `FlexRequestResponse`, and `FlexOrderResponse` will fail on the first send attempt after this PR is deployed. Inbound messaging (signature verification, asset updates) is unaffected. + +**Why V2 worked:** V2 returned a per-participant `endpoint` URL in the address-book response. In a broker model all of those almost certainly pointed at the GOPACS broker itself (the broker forwards to the real DSO backend); the V3 spec removing the field is consistent with "stop pretending per-participant endpoints exist, just POST to the broker". + +**Proposed follow-up fix** (a separate small PR): +1. Add a new config `GOPACS_BROKER_URL` to `GOPACSHandler`, with a sensible default (the broker URL used for message submission — to be confirmed from the GOPACS testing documentation or by asking `servicedesk@gopacs.eu`). +2. Pass this value as the `endpoint` argument when constructing `UftpParticipantInformation` in `getParticipantInformation`. +3. Verify against acceptance by sending a `FlexOffer` in response to a test `FlexRequest`. + +Options to determine the correct broker URL before writing that PR: +- Check the GOPACS testing doc linked from `ems/README.md` (`GOPACS-Testing-receiving-and-sending-flex-messages-by-UFTP-testing-functionality-04-12-2025.pdf`) — it likely names the submit endpoint. +- Email `servicedesk@gopacs.eu`: "V3 ParticipantView drops the `endpoint` field; which URL should outbound UFTP messages be POSTed to?" +- While V2 is still live, hit the current V2 endpoint and inspect the `endpoint` field returned per DSO; if all DSOs share a URL, that is the broker URL. ## Timeline From 2be6b8ddd4253fd08e892f56c3507be87549c38b Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 29 Apr 2026 11:19:02 +0200 Subject: [PATCH 6/9] Updated shapeshifter lib --- .../extension/ems/manager/gopacs/ParticipantView.java | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java index 828b8c6..f956a3a 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/ParticipantView.java @@ -1,5 +1,5 @@ /* - * Copyright 2025, OpenRemote Inc. + * Copyright 2026, OpenRemote Inc. * * See the CONTRIBUTORS.txt file in the distribution for a * full listing of individual contributors. diff --git a/gradle.properties b/gradle.properties index 4d2b7b7..32ec5ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,5 +18,5 @@ openremoteVersion = 1.20.0 bouncyCastleVersion = 1.81 jacksonVersion = 2.21.1 resteasyVersion = 6.2.15.Final -shapeshifterVersion = 3.2.2 +shapeshifterVersion = 3.5.0 testLoggerVersion = 4.0.0 From 7df7c211adf5e92c8e05e26466457c2658a39d67 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 29 Apr 2026 11:19:39 +0200 Subject: [PATCH 7/9] Remove plan file --- GOPACS_PARTICIPANTS_V3_ISSUE.md | 95 --------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 GOPACS_PARTICIPANTS_V3_ISSUE.md diff --git a/GOPACS_PARTICIPANTS_V3_ISSUE.md b/GOPACS_PARTICIPANTS_V3_ISSUE.md deleted file mode 100644 index 4a47bc8..0000000 --- a/GOPACS_PARTICIPANTS_V3_ISSUE.md +++ /dev/null @@ -1,95 +0,0 @@ -# Migrate GOPACS UFTP Participants API from V2 to V3 - -**Label:** Enhancement - -## Background - -GOPACS has announced (email, April 2026) that the **UFTP Participants API V2** is deprecated and will be removed starting **26 October 2026**. The acceptance environment is removed first, then production. Our EMS integration in `ems/manager/gopacs/` calls this API whenever Shapeshifter needs a peer's public key to verify an incoming UFTP message signature, so if we miss the deadline, inbound GOPACS messaging stops working. - -The main functional change in V3 is that participants can now be looked up by **ContractID + role** as well as by domain, which lets GOPACS route activation messages to per-contract handlers. We are **not** adopting that new capability in this migration — it would require restructuring `GOPACSHandler` (currently one handler per EAN) and can be a follow-up if we ever need per-Capacity-Steering-Contract / per-Time-bound-Transport-Right behaviour. This migration is a minimal drop-in replacement to stay functional past the deadline. - -## What's changing in V3 - -| | V2 | V3 | -|---|---|---| -| Base URL (prod) | `https://clc-message-broker.gopacs-services.eu` | `https://api.gopacs-services.eu` | -| Base URL (acc) | *n/a via our config* | `https://api.acc.gopacs-services.eu` | -| Participant lookup | `GET /v2/participants/DSO?contractedEan={ean}` → list of all DSOs for an EAN | `GET /uftp-participants/v3/participants/{uftpDomainName}` → single `ParticipantView` | -| Content type | XML | JSON | -| Auth | (not enforced in our client) | Bearer JWT | -| Response fields | `domain`, `publicKey`, `endpoint`, … | `domain`, `publicKey` only | -| Removed | — | An unspecified "old and temporary endpoint" | - -V3 also adds `GET /uftp-participants/v3/participants/contracts/{contractId}/roles/{uftpRole}` (roles: `AGR`, `DSO`; `CRO` listed as not supported). Our signature-verification call site always has the sender domain so the by-domain endpoint is the direct substitute — we don't need the contract/role endpoint for this migration. - -Swagger documentation: -- Acceptance: https://api.acc.gopacs-services.eu/docs/uftp-broker/v3/api-docs/uftp-participants-v3 -- Production: https://api.gopacs-services.eu/docs/uftp-broker/v3/api-docs/uftp-participants-v3 - -## Scope of this change - -- [x] Update `GOPACSAddressBookResource` to the V3 by-domain endpoint (JSON, Authorization header). -- [x] Add `ParticipantView` DTO record mirroring the V3 response schema. -- [x] Rewrite `GOPACSHandler.getParticipantInformation(...)` to call the V3 endpoint with a Bearer token from the existing OAuth2 client-credentials flow. Extract the token acquisition into a shared `fetchBearerToken()` helper. -- [x] Change `DEFAULT_GOPACS_PARTICIPANT_URL` to `https://api.gopacs-services.eu`. Acceptance environment selected via existing `GOPACS_PARTICIPANT_URL` override. -- [x] Drop the `endpoint` field from cached `UftpParticipantInformation` (V3 no longer returns it — we pass `null`). - -Out of scope (follow-up if needed): -- Per-ContractID / per-role handler registration using `GET /participants/contracts/{contractId}/roles/{uftpRole}`. -- WireMock tests for the V3 lookup — `GOPACSHandler` currently has no unit tests at all, and adding test infrastructure is a larger separate piece of work. -- Upgrading `shapeshifter-core` from `3.2.2` to `3.5.0`. Shapeshifter is not involved in the V2/V3 migration itself (the participants API is a GOPACS-broker-specific REST API, not part of the UFTP protocol). Verified against the 3.5.0 sources: `UftpParticipantInformation`, `ParticipantResolutionService`, and `UftpSendMessageService.doSend()` are unchanged vs. 3.2.2 — so the version bump would not fix the outbound-send defect described below either. A library bump can be a separate PR if desired. - -## Deployment note - -Any deployment that overrides `GOPACS_PARTICIPANT_URL` (for example, pinning acceptance to `https://clc-message-broker-acc.gopacs-services.eu`) **must be updated** to the new host: - -- Production: `GOPACS_PARTICIPANT_URL=https://api.gopacs-services.eu` (or leave unset to use the default). -- Acceptance: `GOPACS_PARTICIPANT_URL=https://api.acc.gopacs-services.eu`. - -## Acceptance criteria - -- [ ] `./gradlew clean build` passes. -- [ ] Manager deploys cleanly with a GOPACS-enabled `EmsGOPACSAsset`. -- [ ] Against the acceptance environment: an inbound signed UFTP `FlexRequest` from a known DSO triggers a successful V3 participant lookup (200 with `publicKey`) and signature verification passes in `processRawMessage`. Downstream `POWER_*` predicted datapoints are written as before. -- [ ] 404 from the V3 endpoint for an unknown domain produces a FINE-level log, not an error. -- [ ] Works in both acceptance (`api.acc.gopacs-services.eu`) and production (`api.gopacs-services.eu`) via the `GOPACS_PARTICIPANT_URL` override. - -## Known follow-up: outbound messaging needs a broker-URL fallback - -V3's `ParticipantView` no longer carries an `endpoint` field, so the cached `UftpParticipantInformation` stores `endpoint=null`. Reading `shapeshifter-core` 3.2.2 (and confirmed identical in 3.5.0), the outbound send path is: - -1. `GOPACSHandler.notifyNewOutgoingMessage` → `UftpSendMessageService.attemptToSendMessage` -2. `UftpSendMessageService.doSend` (line 124–133 in 3.2.2, 135–144 in 3.5.0): - ```java - UftpParticipantInformation participantInformation = participantService.getParticipantInformation(details.recipient()); - String url = participantInformation.endpoint(); // null after this migration - ... - send(signedXml, url, additionalHeaders, MAX_FOLLOW_REDIRECTS); - ``` -3. `UftpSendMessageService.send(...)`: - ```java - var requestBuilder = HttpRequest.newBuilder().uri(new URI(url)) ... // new URI(null) → NullPointerException - ``` - -The surrounding `try/catch` in `send` catches `URISyntaxException | IllegalArgumentException | IOException | InterruptedException`. It does not catch `NullPointerException`, so the NPE escapes `attemptToSendMessage` and is swallowed only by `GOPACSHandler.notifyNewOutgoingMessage`'s generic `catch (Exception e)` as a SEVERE log. - -**Practical impact:** every outbound `FlexOffer`, `FlexRequestResponse`, and `FlexOrderResponse` will fail on the first send attempt after this PR is deployed. Inbound messaging (signature verification, asset updates) is unaffected. - -**Why V2 worked:** V2 returned a per-participant `endpoint` URL in the address-book response. In a broker model all of those almost certainly pointed at the GOPACS broker itself (the broker forwards to the real DSO backend); the V3 spec removing the field is consistent with "stop pretending per-participant endpoints exist, just POST to the broker". - -**Proposed follow-up fix** (a separate small PR): -1. Add a new config `GOPACS_BROKER_URL` to `GOPACSHandler`, with a sensible default (the broker URL used for message submission — to be confirmed from the GOPACS testing documentation or by asking `servicedesk@gopacs.eu`). -2. Pass this value as the `endpoint` argument when constructing `UftpParticipantInformation` in `getParticipantInformation`. -3. Verify against acceptance by sending a `FlexOffer` in response to a test `FlexRequest`. - -Options to determine the correct broker URL before writing that PR: -- Check the GOPACS testing doc linked from `ems/README.md` (`GOPACS-Testing-receiving-and-sending-flex-messages-by-UFTP-testing-functionality-04-12-2025.pdf`) — it likely names the submit endpoint. -- Email `servicedesk@gopacs.eu`: "V3 ParticipantView drops the `endpoint` field; which URL should outbound UFTP messages be POSTed to?" -- While V2 is still live, hit the current V2 endpoint and inspect the `endpoint` field returned per DSO; if all DSOs share a URL, that is the broker URL. - -## Timeline - -- **25 October 2026** — last day GOPACS guarantees V2 is available. -- **26 October 2026** — GOPACS begins removing V2, starting with the acceptance environment. - -Aim to have this merged and deployed to the acceptance environment well ahead of 26 October 2026 so we can validate against acceptance before production removal. From da2c0aa5056966f8b71da7806177d9ae1c85d8d2 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 29 Apr 2026 16:31:16 +0200 Subject: [PATCH 8/9] Fixed setting broker url --- .../extension/ems/manager/gopacs/GOPACSHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java index bed4576..5879a5d 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java @@ -87,6 +87,8 @@ public class GOPACSHandler implements UftpPayloadHandler, UftpParticipantService private static final Logger LOG = SyslogCategory.getLogger(API, GOPACSHandler.class); public static final String GOPACS_PRIVATE_KEY_FILE = "GOPACS_PRIVATE_KEY_FILE"; + public static final String GOPACS_BROKER_URL = "GOPACS_BROKER_URL"; + public static final String DEFAULT_GOPACS_BROKER_URL = "https://clc-message-broker.gopacs-services.eu"; public static final String GOPACS_PARTICIPANT_URL = "GOPACS_PARTICIPANT_URL"; public static final String DEFAULT_GOPACS_PARTICIPANT_URL = "https://api.gopacs-services.eu"; public static final String GOPACS_OAUTH2_URL = "GOPACS_OAUTH2_URL"; @@ -106,6 +108,7 @@ public class GOPACSHandler implements UftpPayloadHandler, UftpParticipantService protected final String contractedEAN; protected final String electricitySupplierAssetId; protected final String realm; + protected final String gopacsBrokerUrl; protected final Map participants; protected final AssetProcessingService assetProcessingService; @@ -159,6 +162,7 @@ protected GOPACSHandler(String contractedEAN, String realm, String electricitySu this.timerService = container.getService(TimerService.class); this.webService = container.getService(WebService.class); + this.gopacsBrokerUrl = container.getConfig().getOrDefault(GOPACS_BROKER_URL, DEFAULT_GOPACS_BROKER_URL); this.responseDelaySeconds = Integer.parseInt(container.getConfig().getOrDefault(GOPACS_RESPONSE_DELAY_SECONDS, DEFAULT_GOPACS_RESPONSE_DELAY_SECONDS)); this.flexOfferDelaySeconds = Integer.parseInt(container.getConfig().getOrDefault(GOPACS_FLEX_OFFER_DELAY_SECONDS, DEFAULT_GOPACS_FLEX_OFFER_DELAY_SECONDS)); @@ -551,7 +555,7 @@ public Optional getParticipantInformation(USEFRoleTy int status = response != null ? response.getStatus() : -1; if (status == 200) { ParticipantView view = response.readEntity(ParticipantView.class); - UftpParticipantInformation info = new UftpParticipantInformation(view.domain(), view.publicKey(), null, true); + UftpParticipantInformation info = new UftpParticipantInformation(view.domain(), view.publicKey(), this.gopacsBrokerUrl + "/shapeshifter/api/v3/message", true); participants.put(view.domain(), info); return Optional.of(info); } From 6479c1b5e91372d4c13cba95c72b2560ddb11418 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Jun 2026 11:58:57 +0200 Subject: [PATCH 9/9] Address AI review findings on GOPACS V3 migration - Re-assert contracted-EAN scoping in processRawMessage: the V3 participant lookup resolves any sender domain, so a validly signed flex message from a participant outside this handler's contracted EAN could be applied to the asset. Drop FlexMessageType messages whose congestion point does not match the contracted EAN before any asset mutation or outbound reply. - Skip the participant API call and return empty when no OAuth2 bearer token is available, instead of sending an invalid Authorization header. - Warn when the outbound authorization header has no bearer token. - Strip trailing slashes from GOPACS_BROKER_URL so the synthesised endpoint never contains a double slash. --- .../ems/manager/gopacs/GOPACSHandler.java | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java index 5879a5d..0871d66 100644 --- a/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java +++ b/ems/src/main/java/org/openremote/extension/ems/manager/gopacs/GOPACSHandler.java @@ -162,7 +162,8 @@ protected GOPACSHandler(String contractedEAN, String realm, String electricitySu this.timerService = container.getService(TimerService.class); this.webService = container.getService(WebService.class); - this.gopacsBrokerUrl = container.getConfig().getOrDefault(GOPACS_BROKER_URL, DEFAULT_GOPACS_BROKER_URL); + // Strip trailing slashes so the synthesised broker endpoint never contains a double slash + this.gopacsBrokerUrl = container.getConfig().getOrDefault(GOPACS_BROKER_URL, DEFAULT_GOPACS_BROKER_URL).replaceAll("/+$", ""); this.responseDelaySeconds = Integer.parseInt(container.getConfig().getOrDefault(GOPACS_RESPONSE_DELAY_SECONDS, DEFAULT_GOPACS_RESPONSE_DELAY_SECONDS)); this.flexOfferDelaySeconds = Integer.parseInt(container.getConfig().getOrDefault(GOPACS_FLEX_OFFER_DELAY_SECONDS, DEFAULT_GOPACS_FLEX_OFFER_DELAY_SECONDS)); @@ -296,7 +297,11 @@ public void notifyNewOutgoingMessage(OutgoingUftpMessage getParticipantInformation(USEFRoleTy } String authorization = fetchBearerToken(); + if (authorization.isBlank()) { + LOG.warning("Skipping participant lookup for " + domain + ": no OAuth2 bearer token available"); + return Optional.empty(); + } try (Response response = gopacsAddressBookResource.fetchParticipantByDomain(authorization, domain)) { int status = response != null ? response.getStatus() : -1; if (status == 200) {