Skip to content

Commit 444b713

Browse files
dfcoffinclaude
andauthored
feat: Add ESPI 4.0 XSD compliance to Authorization with customerResourceURI (GreenButtonAlliance#38)
BREAKING CHANGE: AuthorizationDto XML element order changed to match ESPI 4.0 XSD specification ## Changes ### AuthorizationEntity.java - Add customerResourceURI field for PII Subscription API access (ESPI 4.0 requirement) - Remove merge() method (only CRUD Gets supported initially) - Remove unlink() method (Spring Data JPA handles relationship cleanup) - Update toString() to include customerResourceURI ### AuthorizationDto.java (BREAKING CHANGE) - **Reorder propOrder to match ESPI 4.0 XSD sequence** - Add missing XSD-compliant fields: - authorizedPeriod (DateTimeIntervalDto) - publishedPeriod (DateTimeIntervalDto) - customerResourceURI (String) - primary goal - error (String) - errorDescription (String) - errorUri (String) - Mark OAuth2 implementation fields as @XmlTransient (accessToken, refreshToken, etc.) - Add OpenAPI @Schema annotations for API documentation ### AuthorizationMapper.java - Add mappings for all new XSD-compliant fields - Update toDto(), toEntity(), and updateEntity() methods ### pom.xml - Add swagger-annotations-jakarta dependency (v2.2.22) for @Schema annotations ## Breaking Change Details **XML element order has changed to comply with ESPI 4.0 XSD:** - Before: Incorrect, non-compliant ordering - After: ESPI 4.0 compliant sequence matching espi.xsd lines 264-343 **Impact:** - Most XML parsers (JAXB, DOM, SAX) are order-independent and should be unaffected - Strict XSD validation will now PASS instead of FAIL - XPath position selectors may need updates - XML diff tools will show reordering changes ## Testing - All 14 AuthorizationRepositoryTest tests pass - Verified with: mvn test -Dtest=AuthorizationRepositoryTest - Compilation successful with new Swagger dependency ## References - ESPI 4.0 XSD: openespi-common/src/main/resources/schema/ESPI_4.0/espi.xsd - Database column customer_resource_uri already exists (V1 migration line 279) - No database migration needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent 22730ca commit 444b713

File tree

4 files changed

+254
-127
lines changed

4 files changed

+254
-127
lines changed

openespi-common/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,13 @@
508508
<scope>provided</scope>
509509
</dependency>
510510

511+
<!-- Swagger/OpenAPI Annotations for API documentation -->
512+
<dependency>
513+
<groupId>io.swagger.core.v3</groupId>
514+
<artifactId>swagger-annotations-jakarta</artifactId>
515+
<version>2.2.22</version>
516+
</dependency>
517+
511518
<!-- Flyway and database-specific dependencies moved to application pom.xml files -->
512519

513520
<!-- TestContainers for integration testing -->

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AuthorizationEntity.java

Lines changed: 11 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ public class AuthorizationEntity extends IdentifiedObject {
194194
@Column(name = "authorization_uri", length = 512)
195195
private String authorizationURI;
196196

197+
/**
198+
* URI for accessing PII data the Third Party is authorized to access.
199+
* Used in GET of the PII resource subscription.
200+
* Note: This URI will have a different namespace than resourceURI.
201+
*
202+
* @see <a href="https://www.naesb.org/ESPI_Standards.asp">NAESB ESPI 4.0</a>
203+
*/
204+
@Column(name = "customer_resource_uri", length = 512)
205+
private String customerResourceURI;
206+
197207
/**
198208
* Third party identifier.
199209
* Identifies the third-party application or organization.
@@ -280,51 +290,6 @@ protected String generateDefaultUpHref() {
280290
return getUpHref();
281291
}
282292

283-
/**
284-
* Merges data from another AuthorizationEntity.
285-
* Updates authorization parameters while preserving relationships.
286-
*
287-
* @param other the other authorization entity to merge from
288-
*/
289-
public void merge(AuthorizationEntity other) {
290-
if (other != null) {
291-
super.merge(other);
292-
293-
// Update authorization parameters
294-
this.authorizedPeriod = other.authorizedPeriod;
295-
this.publishedPeriod = other.publishedPeriod;
296-
this.status = other.status;
297-
this.expiresIn = other.expiresIn;
298-
this.grantType = other.grantType;
299-
this.scope = other.scope;
300-
this.responseType = other.responseType;
301-
this.tokenType = other.tokenType;
302-
this.error = other.error;
303-
this.errorDescription = other.errorDescription;
304-
this.errorUri = other.errorUri;
305-
this.resourceURI = other.resourceURI;
306-
this.authorizationURI = other.authorizationURI;
307-
this.thirdParty = other.thirdParty;
308-
309-
// Note: Sensitive fields like tokens are not merged
310-
// Note: Relationships are not merged to preserve existing associations
311-
}
312-
}
313-
314-
/**
315-
* Clears all relationships when unlinking the entity.
316-
* Simplified - applications handle relationship cleanup.
317-
*/
318-
public void unlink() {
319-
clearRelatedLinks();
320-
321-
// Simple field clearing - applications handle bidirectional cleanup
322-
this.retailCustomer = null;
323-
this.subscription = null;
324-
325-
// Note: applicationInformation is not cleared as it might be referenced elsewhere
326-
}
327-
328293
/**
329294
* Checks if this authorization is currently active.
330295
*
@@ -516,6 +481,7 @@ public String toString() {
516481
"errorUri = " + getErrorUri() + ", " +
517482
"resourceURI = " + getResourceURI() + ", " +
518483
"authorizationURI = " + getAuthorizationURI() + ", " +
484+
"customerResourceURI = " + getCustomerResourceURI() + ", " +
519485
"thirdParty = " + getThirdParty() + ", " +
520486
"description = " + getDescription() + ", " +
521487
"created = " + getCreated() + ", " +

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/AuthorizationDto.java

Lines changed: 172 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,95 +19,208 @@
1919

2020
package org.greenbuttonalliance.espi.common.dto.usage;
2121

22+
import io.swagger.v3.oas.annotations.media.Schema;
2223
import jakarta.xml.bind.annotation.*;
2324

2425
/**
2526
* Authorization DTO record for JAXB XML marshalling/unmarshalling.
26-
*
27+
*
2728
* Represents OAuth 2.0 authorization for third-party access to Green Button data.
29+
* Complies with NAESB ESPI 4.0 XSD specification.
30+
*
31+
* @see <a href="https://www.naesb.org/ESPI_Standards.asp">NAESB ESPI 4.0</a>
2832
*/
2933
@XmlRootElement(name = "Authorization", namespace = "http://naesb.org/espi")
3034
@XmlAccessorType(XmlAccessType.PROPERTY)
3135
@XmlType(name = "Authorization", namespace = "http://naesb.org/espi", propOrder = {
32-
"accessToken", "authorizationUri", "applicationInformationId", "retailCustomerId",
33-
"resourceURI", "scope", "status", "expiresIn", "grantType", "refreshToken",
34-
"tokenType", "thirdParty", "ppid", "authorizationCode"
36+
"authorizedPeriod", // ESPI 4.0 XSD sequence
37+
"publishedPeriod",
38+
"status",
39+
"expiresIn", // Maps to expires_at in XML
40+
"grantType",
41+
"scope",
42+
"tokenType",
43+
"error",
44+
"errorDescription",
45+
"errorUri",
46+
"resourceURI",
47+
"authorizationUri",
48+
"customerResourceURI" // NEW - PII subscription URI
3549
})
3650
public record AuthorizationDto(
37-
51+
52+
// UUID (not in XSD - internal use only)
53+
@XmlTransient
3854
String uuid,
39-
String accessToken,
40-
String authorizationUri,
41-
String applicationInformationId,
42-
String retailCustomerId,
43-
String resourceURI,
44-
String scope,
55+
56+
// XSD-compliant fields (in order)
57+
58+
@Schema(description = "Period during which this authorization is valid",
59+
example = "{\"start\": 1704067200, \"duration\": 31536000}")
60+
DateTimeIntervalDto authorizedPeriod,
61+
62+
@Schema(description = "Period during which data was published",
63+
example = "{\"start\": 1704067200, \"duration\": 31536000}")
64+
DateTimeIntervalDto publishedPeriod,
65+
66+
@Schema(description = "Authorization status (1=ACTIVE, 2=REVOKED, 3=EXPIRED, 4=PENDING)",
67+
example = "1")
4568
Short status,
69+
70+
@Schema(description = "Expiration timestamp (epoch seconds)",
71+
example = "1735689600")
4672
Long expiresIn,
73+
74+
@Schema(description = "OAuth2 grant type",
75+
example = "authorization_code",
76+
allowableValues = {"authorization_code", "client_credentials", "refresh_token"})
4777
String grantType,
48-
String refreshToken,
78+
79+
@Schema(description = "OAuth2 scope defining permissions",
80+
example = "FB=1_3_4_5_13_14_39;IntervalDuration=3600",
81+
required = true)
82+
String scope,
83+
84+
@Schema(description = "OAuth2 token type",
85+
example = "Bearer",
86+
allowableValues = {"Bearer"})
4987
String tokenType,
88+
89+
@Schema(description = "OAuth2 error code if authorization failed",
90+
example = "invalid_grant")
91+
String error,
92+
93+
@Schema(description = "Human-readable error description",
94+
example = "The provided authorization grant is invalid")
95+
String errorDescription,
96+
97+
@Schema(description = "URI with more information about the error",
98+
example = "https://example.com/oauth/errors/invalid_grant")
99+
String errorUri,
100+
101+
@Schema(description = "URI for accessing the authorized energy usage resource",
102+
example = "https://api.example.com/espi/1_1/resource/Batch/Subscription/12345",
103+
required = true)
104+
String resourceURI,
105+
106+
@Schema(description = "URI for managing this authorization",
107+
example = "https://api.example.com/espi/1_1/resource/Authorization/67890",
108+
required = true)
109+
String authorizationUri,
110+
111+
@Schema(description = "URI for accessing PII data the Third Party is authorized to access. Points to PII resource subscription endpoint with different namespace than resourceURI.",
112+
example = "https://api.example.com/customer/espi/1_1/resource/Batch/RetailCustomer/12345")
113+
String customerResourceURI,
114+
115+
// OAuth2 implementation fields (not in ESPI XSD - marked as @XmlTransient)
116+
117+
@XmlTransient
118+
@Schema(description = "OAuth2 access token (not included in XML for security)",
119+
example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
120+
String accessToken,
121+
122+
@XmlTransient
123+
@Schema(description = "OAuth2 refresh token (not included in XML for security)",
124+
example = "def50200...")
125+
String refreshToken,
126+
127+
@XmlTransient
128+
@Schema(description = "OAuth2 authorization code (temporary, not in XML)",
129+
example = "abc123xyz")
130+
String authorizationCode,
131+
132+
@XmlTransient
133+
@Schema(description = "OAuth2 state parameter for CSRF protection (not in ESPI XSD)",
134+
example = "xyz789")
135+
String state,
136+
137+
@XmlTransient
138+
@Schema(description = "OAuth2 response type (not in ESPI XSD)",
139+
example = "code")
140+
String responseType,
141+
142+
@XmlTransient
143+
@Schema(description = "Third party application identifier (not in ESPI XSD)",
144+
example = "ThirdPartyApp")
50145
String thirdParty,
51-
String ppid,
52-
String authorizationCode
146+
147+
@XmlTransient
148+
@Schema(description = "Application information ID (relationship, not in XSD)",
149+
example = "550e8400-e29b-41d4-a716-446655440000")
150+
String applicationInformationId,
151+
152+
@XmlTransient
153+
@Schema(description = "Retail customer ID (relationship, not in XSD for privacy)",
154+
example = "660e8400-e29b-41d4-a716-446655440000")
155+
String retailCustomerId
53156
) {
54-
157+
55158
/**
56159
* Default constructor for JAXB.
57160
*/
58161
public AuthorizationDto() {
59-
this(null, null, null, null, null, null, null, null, null,
60-
null, null, null, null, null, null);
162+
this(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
163+
null, null, null, null, null, null, null, null);
61164
}
62-
165+
63166
/**
64-
* Constructor for basic authorization.
167+
* Constructor for basic authorization (XSD-compliant fields only).
65168
*/
66-
public AuthorizationDto(String accessToken, String scope, String retailCustomerId) {
67-
this(null, accessToken, null, null, retailCustomerId, null, scope, null, null,
68-
null, null, null, null, null, null);
169+
public AuthorizationDto(String scope, Short status, String resourceURI, String authorizationUri) {
170+
this(null, null, null, status, null, null, scope, null, null, null, null,
171+
resourceURI, authorizationUri, null, null, null, null, null, null, null, null, null);
69172
}
70-
71-
// JAXB property accessors
72-
@XmlElement(name = "accessToken", namespace = "http://naesb.org/espi")
73-
public String getAccessToken() { return accessToken; }
74-
75-
@XmlElement(name = "authorizationURI", namespace = "http://naesb.org/espi")
76-
public String getAuthorizationUri() { return authorizationUri; }
77-
78-
@XmlElement(name = "applicationInformationId", namespace = "http://naesb.org/espi")
79-
public String getApplicationInformationId() { return applicationInformationId; }
80-
81-
@XmlElement(name = "retailCustomerId", namespace = "http://naesb.org/espi")
82-
public String getRetailCustomerId() { return retailCustomerId; }
83-
84-
@XmlElement(name = "resourceURI", namespace = "http://naesb.org/espi")
85-
public String getResourceURI() { return resourceURI; }
86-
87-
@XmlElement(name = "scope", namespace = "http://naesb.org/espi")
88-
public String getScope() { return scope; }
89-
90-
@XmlElement(name = "status", namespace = "http://naesb.org/espi")
173+
174+
// JAXB property accessors for XSD-compliant fields (in propOrder sequence)
175+
176+
@XmlElement(name = "authorizedPeriod", namespace = "http://naesb.org/espi")
177+
public DateTimeIntervalDto getAuthorizedPeriod() { return authorizedPeriod; }
178+
179+
@XmlElement(name = "publishedPeriod", namespace = "http://naesb.org/espi")
180+
public DateTimeIntervalDto getPublishedPeriod() { return publishedPeriod; }
181+
182+
@XmlElement(name = "status", namespace = "http://naesb.org/espi", required = true)
91183
public Short getStatus() { return status; }
92-
93-
@XmlElement(name = "expires_in", namespace = "http://naesb.org/espi")
184+
185+
@XmlElement(name = "expires_at", namespace = "http://naesb.org/espi", required = true)
94186
public Long getExpiresIn() { return expiresIn; }
95-
187+
96188
@XmlElement(name = "grant_type", namespace = "http://naesb.org/espi")
97189
public String getGrantType() { return grantType; }
98-
99-
@XmlElement(name = "refresh_token", namespace = "http://naesb.org/espi")
100-
public String getRefreshToken() { return refreshToken; }
101-
102-
@XmlElement(name = "token_type", namespace = "http://naesb.org/espi")
190+
191+
@XmlElement(name = "scope", namespace = "http://naesb.org/espi", required = true)
192+
public String getScope() { return scope; }
193+
194+
@XmlElement(name = "token_type", namespace = "http://naesb.org/espi", required = true)
103195
public String getTokenType() { return tokenType; }
104-
105-
@XmlElement(name = "third_party", namespace = "http://naesb.org/espi")
106-
public String getThirdParty() { return thirdParty; }
107-
108-
@XmlElement(name = "ppid", namespace = "http://naesb.org/espi")
109-
public String getPpid() { return ppid; }
110-
111-
@XmlElement(name = "code", namespace = "http://naesb.org/espi")
196+
197+
@XmlElement(name = "error", namespace = "http://naesb.org/espi")
198+
public String getError() { return error; }
199+
200+
@XmlElement(name = "error_description", namespace = "http://naesb.org/espi")
201+
public String getErrorDescription() { return errorDescription; }
202+
203+
@XmlElement(name = "error_uri", namespace = "http://naesb.org/espi")
204+
public String getErrorUri() { return errorUri; }
205+
206+
@XmlElement(name = "resourceURI", namespace = "http://naesb.org/espi", required = true)
207+
public String getResourceURI() { return resourceURI; }
208+
209+
@XmlElement(name = "authorizationURI", namespace = "http://naesb.org/espi", required = true)
210+
public String getAuthorizationUri() { return authorizationUri; }
211+
212+
@XmlElement(name = "customerResourceURI", namespace = "http://naesb.org/espi")
213+
public String getCustomerResourceURI() { return customerResourceURI; }
214+
215+
// OAuth2 implementation field accessors (marked @XmlTransient, not in XML output)
216+
217+
public String getUuid() { return uuid; }
218+
public String getAccessToken() { return accessToken; }
219+
public String getRefreshToken() { return refreshToken; }
112220
public String getAuthorizationCode() { return authorizationCode; }
221+
public String getState() { return state; }
222+
public String getResponseType() { return responseType; }
223+
public String getThirdParty() { return thirdParty; }
224+
public String getApplicationInformationId() { return applicationInformationId; }
225+
public String getRetailCustomerId() { return retailCustomerId; }
113226
}

0 commit comments

Comments
 (0)