diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/ApplicationInformationRESTController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/ApplicationInformationRESTController.java
index daefed2c..d0dafe84 100755
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/ApplicationInformationRESTController.java
+++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/ApplicationInformationRESTController.java
@@ -37,18 +37,17 @@
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
-import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.io.ByteArrayOutputStream;
-import java.net.URI;
import java.util.List;
import java.util.UUID;
/**
- * Modern RESTful controller for managing ApplicationInformation resources according to the
+ * Modern RESTful controller for reading ApplicationInformation resources according to the
* Green Button Alliance ESPI (Energy Services Provider Interface) specification.
*
- * This controller uses ApplicationInformationService and modern Spring Boot 3.5 patterns.
+ * GET-only. The CRUD write endpoints (POST/PUT/DELETE) are deferred — they are admin/sandbox-DB
+ * management APIs to be delivered in the separate admin-CRUD track (see issue #119 build plan).
*/
@RestController
@RequestMapping("/espi/1_1/resource/ApplicationInformation")
@@ -119,101 +118,4 @@ public ResponseEntity getApplicationInformation(
.contentType(MediaType.APPLICATION_XML)
.body(out.toByteArray());
}
-
- /**
- * Creates a new ApplicationInformation resource.
- *
- * @param dto ApplicationInformationDto to create
- * @return created ApplicationInformationDto
- */
- @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
- @Operation(
- summary = "Create ApplicationInformation",
- description = "Creates a new ApplicationInformation resource"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "201", description = "Successfully created ApplicationInformation"),
- @ApiResponse(responseCode = "400", description = "Invalid ApplicationInformation data")
- })
- @PreAuthorize("hasAuthority('SCOPE_DataCustodian_Admin_Access')")
- public ResponseEntity createApplicationInformation(
- @Parameter(description = "ApplicationInformation data to create", required = true)
- @RequestBody ApplicationInformationDto dto) {
-
- ApplicationInformationEntity entity = applicationInformationService.fromDto(dto);
- ApplicationInformationEntity savedEntity = applicationInformationService.save(entity);
-
- URI location = ServletUriComponentsBuilder.fromCurrentRequest()
- .path("/{id}")
- .buildAndExpand(savedEntity.getId())
- .toUri();
-
- return ResponseEntity.created(location).body(savedEntity);
- }
-
- /**
- * Updates an existing ApplicationInformation resource.
- *
- * @param applicationInformationId Unique identifier for the ApplicationInformation to update
- * @param dto Updated ApplicationInformationDto data
- * @return updated ApplicationInformationEntity
- */
- @PutMapping(value = "/{applicationInformationId}",
- consumes = MediaType.APPLICATION_JSON_VALUE,
- produces = MediaType.APPLICATION_JSON_VALUE)
- @Operation(
- summary = "Update ApplicationInformation",
- description = "Updates an existing ApplicationInformation resource"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Successfully updated ApplicationInformation"),
- @ApiResponse(responseCode = "404", description = "ApplicationInformation not found"),
- @ApiResponse(responseCode = "400", description = "Invalid ApplicationInformation data")
- })
- @PreAuthorize("hasAuthority('SCOPE_DataCustodian_Admin_Access')")
- public ResponseEntity updateApplicationInformation(
- @Parameter(description = "Unique identifier of the ApplicationInformation to update", required = true)
- @PathVariable UUID applicationInformationId,
- @Parameter(description = "Updated ApplicationInformation data", required = true)
- @RequestBody ApplicationInformationDto dto) {
-
- ApplicationInformationEntity existing = applicationInformationService.findById(applicationInformationId);
- if (existing == null) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "ApplicationInformation not found");
- }
-
- ApplicationInformationEntity entity = applicationInformationService.fromDto(dto);
- entity.setId(applicationInformationId);
- ApplicationInformationEntity updatedEntity = applicationInformationService.save(entity);
-
- return ResponseEntity.ok(updatedEntity);
- }
-
- /**
- * Deletes an ApplicationInformation resource.
- *
- * @param applicationInformationId Unique identifier for the ApplicationInformation to delete
- */
- @DeleteMapping("/{applicationInformationId}")
- @Operation(
- summary = "Delete ApplicationInformation",
- description = "Deletes an ApplicationInformation resource"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "204", description = "Successfully deleted ApplicationInformation"),
- @ApiResponse(responseCode = "404", description = "ApplicationInformation not found")
- })
- @PreAuthorize("hasAuthority('SCOPE_DataCustodian_Admin_Access')")
- public ResponseEntity deleteApplicationInformation(
- @Parameter(description = "Unique identifier of the ApplicationInformation to delete", required = true)
- @PathVariable UUID applicationInformationId) {
-
- ApplicationInformationEntity existing = applicationInformationService.findById(applicationInformationId);
- if (existing == null) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "ApplicationInformation not found");
- }
-
- applicationInformationService.deleteById(applicationInformationId);
- return ResponseEntity.noContent().build();
- }
-}
\ No newline at end of file
+}
diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/AuthorizationRESTController.java.disabled b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/AuthorizationRESTController.java.disabled
deleted file mode 100755
index 22c2acb6..00000000
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/AuthorizationRESTController.java.disabled
+++ /dev/null
@@ -1,429 +0,0 @@
-/*
- *
- * Copyright (c) 2025 Green Button Alliance, Inc.
- *
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.greenbuttonalliance.espi.datacustodian.web.api;
-
-import com.sun.syndication.io.FeedException;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.greenbuttonalliance.espi.common.domain.usage.AuthorizationEntity;
-import org.greenbuttonalliance.espi.common.service.AuthorizationService;
-import org.greenbuttonalliance.espi.common.service.RetailCustomerService;
-import org.greenbuttonalliance.espi.common.service.DtoExportService;
-import org.greenbuttonalliance.espi.datacustodian.utils.VerifyURLParams;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Map;
-
-/**
- * RESTful controller for managing AuthorizationEntity resources according to the
- * Green Button Alliance ESPI (Energy Services Provider Interface) specification.
- *
- * AuthorizationEntity represents OAuth2 access permissions and tokens for
- * third-party applications to access customer energy data.
- */
-// @RestController - COMMENTED OUT: Replaced with modern AuthorizationController
-// @RequestMapping("/espi/1_1/resource")
-// @Tag(name = "Authorization", description = "OAuth2 AuthorizationEntity and Access Token Management API")
-// @Component
-public class AuthorizationRESTController {
-
- private final AuthorizationService authorizationService;
- private final RetailCustomerService retailCustomerService;
- private final DtoExportService dtoExportService;
-
- @Autowired
- public AuthorizationRESTController(
- AuthorizationService authorizationService,
- RetailCustomerService retailCustomerService,
- DtoExportService dtoExportService) {
- this.authorizationService = authorizationService;
- this.retailCustomerService = retailCustomerService;
- this.dtoExportService = dtoExportService;
- }
-
- @ExceptionHandler(Exception.class)
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- public void handleGenericException() {
- // Generic exception handler
- }
-
- // ================================
- // ROOT AuthorizationEntity Collection APIs
- // ================================
-
- /**
- * Retrieves all AuthorizationEntity resources (root level access).
- *
- * @param request HTTP servlet request for authorization context
- * @param response HTTP response for streaming ATOM XML content
- * @param params Query parameters for filtering and pagination
- * @throws IOException if output stream cannot be written
- * @throws FeedException if ATOM feed generation fails
- */
- @GetMapping(value = "/Authorization", produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Get AuthorizationEntity Collection",
- description = "Retrieves all authorized AuthorizationEntity resources with optional filtering and pagination. " +
- "Returns an ATOM feed containing OAuth2 authorization and access token information. " +
- "Access scope depends on the requesting client's authorization level."
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Successfully retrieved AuthorizationEntity collection",
- content = @Content(mediaType = MediaType.APPLICATION_ATOM_XML_VALUE,
- schema = @Schema(description = "ATOM feed containing AuthorizationEntity entries"))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "Invalid query parameters provided"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized access to authorization data"
- )
- })
- public void getAuthorizationCollection(
- HttpServletRequest request,
- HttpServletResponse response,
- @Parameter(description = "Query parameters for filtering (published-max, published-min, updated-max, updated-min, max-results, start-index)")
- @RequestParam Map params) throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if (!VerifyURLParams.verifyEntries("/espi/1_1/resource/Authorization", params)) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST,
- "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
-
- try {
- String accessToken = request.getHeader("authorization").replace(
- "Bearer ", "");
- AuthorizationEntity authorization = authorizationService.findByAccessToken(accessToken);
-
- // TODO: Implement authorization export methods in DtoExportService
- // Determine access scope: data_custodian_admin gets everything,
- // third-party clients get restricted scope
- if (authorization.getApplicationInformation().getClientId()
- .equals("data_custodian_admin")) {
- // // dtoExportService.exportAuthorization(...); params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } else {
- // Third-party access with restricted scope
- // // dtoExportService.exportAuthorization(...); params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- }
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- /**
- * Retrieves a specific AuthorizationEntity resource by ID (root level access).
- *
- * @param response HTTP response for streaming ATOM XML content
- * @param authorizationId Unique identifier for the Authorization
- * @param params Query parameters for export filtering
- * @throws IOException if output stream cannot be written
- * @throws FeedException if ATOM entry generation fails
- */
- @GetMapping(value = "/Authorization/{authorizationId}", produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Get AuthorizationEntity by ID",
- description = "Retrieves a specific AuthorizationEntity resource by its unique identifier. " +
- "Returns an ATOM entry containing OAuth2 authorization details including " +
- "access tokens, scope, and application information."
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Successfully retrieved Authorization",
- content = @Content(mediaType = MediaType.APPLICATION_ATOM_XML_VALUE,
- schema = @Schema(description = "ATOM entry containing AuthorizationEntity details"))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "Invalid authorizationId or query parameters"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized access to this authorization data"
- ),
- @ApiResponse(
- responseCode = "404",
- description = "AuthorizationEntity not found"
- )
- })
- public void getAuthorization(
- HttpServletResponse response,
- @Parameter(description = "Unique identifier of the Authorization", required = true)
- @PathVariable Long authorizationId,
- @Parameter(description = "Query parameters for export filtering")
- @RequestParam Map params) throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if (!VerifyURLParams.verifyEntries("/espi/1_1/resource/Authorization/{authorizationId}", params)) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST,
- "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
-
- try {
- // dtoExportService.exportAuthorization(...); params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @PostMapping(value = "/Authorization",
- consumes = MediaType.APPLICATION_ATOM_XML_VALUE,
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Create Authorization",
- description = "Creates a new AuthorizationEntity resource for OAuth2 access control."
- )
- public void createAuthorization(
- HttpServletResponse response,
- @RequestParam Map params,
- @RequestBody InputStream stream) throws IOException {
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- try {
- AuthorizationEntity authorization =
- this.authorizationService.importResource(stream);
- // dtoExportService.exportAuthorization(...); params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @PutMapping(value = "/Authorization/{authorizationId}",
- consumes = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Update Authorization",
- description = "Updates an existing AuthorizationEntity resource."
- )
- public void updateAuthorization(
- HttpServletResponse response,
- @PathVariable Long authorizationId,
- @RequestParam Map params,
- @RequestBody InputStream stream) throws IOException, FeedException {
-
- try {
- AuthorizationEntity authorization =
- authorizationService.findById(authorizationId);
-
- if (authorization != null) {
- AuthorizationEntity newAuthorizationEntity =
- authorizationService.importResource(stream);
- authorization.merge(newAuthorizationEntity);
- response.setStatus(HttpServletResponse.SC_OK);
- } else {
- response.setStatus(HttpServletResponse.SC_NOT_FOUND);
- }
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @DeleteMapping("/Authorization/{authorizationId}")
- @Operation(
- summary = "Delete Authorization",
- description = "Removes an AuthorizationEntity resource and revokes associated OAuth2 tokens."
- )
- public void deleteAuthorization(
- HttpServletResponse response,
- @PathVariable Long authorizationId) {
-
- try {
- AuthorizationEntity authorization = authorizationService.findById(authorizationId);
-
- if (authorization != null) {
- String accessToken = authorization.getAccessToken();
- authorizationService.delete(authorization);
- // Note: Token revocation handled by OAuth2 server
- response.setStatus(HttpServletResponse.SC_OK);
- } else {
- response.setStatus(HttpServletResponse.SC_NOT_FOUND);
- }
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- // =============================================
- // RetailCustomer-scoped AuthorizationEntity Collection APIs
- // =============================================
-
- @GetMapping(value = "/RetailCustomer/{retailCustomerId}/Authorization", produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Get Customer Authorizations",
- description = "Retrieves all AuthorizationEntity resources for a specific retail customer."
- )
- public void getCustomerAuthorizations(
- HttpServletResponse response,
- @PathVariable Long retailCustomerId,
- @RequestParam Map params) throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if (!VerifyURLParams.verifyEntries("/espi/1_1/resource/RetailCustomer/{retailCustomerId}/Authorization", params)) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST,
- "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- try {
- // dtoExportService.exportAuthorization(...); params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @GetMapping(value = "/RetailCustomer/{retailCustomerId}/Authorization/{authorizationId}", produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Get Customer AuthorizationEntity by ID",
- description = "Retrieves a specific AuthorizationEntity resource for a retail customer."
- )
- public void getCustomerAuthorization(
- HttpServletResponse response,
- @PathVariable Long retailCustomerId,
- @PathVariable Long authorizationId,
- @RequestParam Map params) throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if (!VerifyURLParams.verifyEntries("/espi/1_1/resource/RetailCustomer/{retailCustomerId}/Authorization/{authorizationId}", params)) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST,
- "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- try {
- // dtoExportService.exportAuthorization(retailCustomerId, authorizationId, response.getOutputStream(), params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @PostMapping(value = "/RetailCustomer/{retailCustomerId}/Authorization",
- consumes = MediaType.APPLICATION_ATOM_XML_VALUE,
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Create Customer Authorization",
- description = "Creates a new AuthorizationEntity resource for a specific retail customer."
- )
- public void createCustomerAuthorization(
- HttpServletResponse response,
- @PathVariable Long retailCustomerId,
- @RequestParam Map params,
- @RequestBody InputStream stream) throws IOException {
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- try {
- AuthorizationEntity authorization =
- this.authorizationService.importResource(stream);
- // retailCustomerService.associateByUUID(retailCustomerId, authorization.getUUID());\n\t\t\t// TODO: Associate authorization with retail customer
- // dtoExportService.exportAuthorization(retailCustomerId, authorization.getId(), response.getOutputStream(), params);
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @PutMapping(value = "/RetailCustomer/{retailCustomerId}/Authorization/{authorizationId}",
- consumes = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Update Customer Authorization",
- description = "Updates an existing AuthorizationEntity resource for a retail customer."
- )
- public void updateCustomerAuthorization(
- HttpServletResponse response,
- @PathVariable Long retailCustomerId,
- @PathVariable Long authorizationId,
- @RequestParam Map params,
- @RequestBody InputStream stream) throws IOException, FeedException {
-
- try {
- AuthorizationEntity authorization = authorizationService.findById(
- retailCustomerId, authorizationId);
-
- if (authorization != null) {
- AuthorizationEntity newAuthorizationEntity =
- authorizationService.importResource(stream);
- authorization.merge(newAuthorizationEntity);
- response.setStatus(HttpServletResponse.SC_OK);
- } else {
- response.setStatus(HttpServletResponse.SC_NOT_FOUND);
- }
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- @DeleteMapping("/RetailCustomer/{retailCustomerId}/Authorization/{authorizationId}")
- @Operation(
- summary = "Delete Customer Authorization",
- description = "Removes an AuthorizationEntity resource for a retail customer and revokes associated OAuth2 tokens."
- )
- public void deleteCustomerAuthorization(
- HttpServletResponse response,
- @PathVariable Long retailCustomerId,
- @PathVariable Long authorizationId) {
-
- try {
- AuthorizationEntity authorization = authorizationService.findById(
- retailCustomerId, authorizationId);
-
- if (authorization != null) {
- String accessToken = authorization.getAccessToken();
- authorizationService.delete(authorization);
- // Note: Token revocation handled by OAuth2 server
- response.setStatus(HttpServletResponse.SC_OK);
- } else {
- response.setStatus(HttpServletResponse.SC_NOT_FOUND);
- }
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
-
-}
diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/BatchRESTController.java.disabled b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/BatchRESTController.java.disabled
deleted file mode 100644
index 6d012774..00000000
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/BatchRESTController.java.disabled
+++ /dev/null
@@ -1,643 +0,0 @@
-/*
- *
- * Copyright (c) 2025 Green Button Alliance, Inc.
- *
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.greenbuttonalliance.espi.datacustodian.web.api;
-
-import com.sun.syndication.io.FeedException;
-import org.greenbuttonalliance.espi.common.domain.usage.ApplicationInformationEntity;
-import org.greenbuttonalliance.espi.common.domain.usage.AuthorizationEntity;
-import org.greenbuttonalliance.espi.common.domain.usage.RetailCustomerEntity;
-import org.greenbuttonalliance.espi.common.service.*;
-import org.greenbuttonalliance.espi.common.repositories.usage.*;
-import org.greenbuttonalliance.espi.datacustodian.utils.VerifyURLParams;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.MediaType;
-import org.springframework.stereotype.Controller;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Map;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-// TODO: Re-enable after implementing missing services (ImportService, NotificationService)
-// @RestController
-// @RequestMapping("/espi/1_1/resource")
-// @Tag(name = "Batch Operations", description = "Green Button Bulk Data Processing API")
-// COMMENTED OUT: Complex batch operations not needed for basic OAuth2 resource server functionality
-// @Component
-public class BatchRESTController {
-
- // TODO: Implement these services in openespi-common
- // private final ImportService importService;
- // private final ResourceRepository resourceService;
- private final AuthorizationService authorizationService;
- // private final NotificationService notificationService;
- private final RetailCustomerService retailCustomerService;
- private final DtoExportService exportService;
-
- @Autowired
- public BatchRESTController(
- // ImportService importService,
- // ResourceRepository resourceService,
- AuthorizationService authorizationService,
- // NotificationService notificationService,
- RetailCustomerService retailCustomerService,
- DtoExportService exportService) {
- // this.importService = importService;
- // this.resourceService = resourceService;
- this.authorizationService = authorizationService;
- // this.notificationService = notificationService;
- this.retailCustomerService = retailCustomerService;
- this.exportService = exportService;
- }
-
- /**
- * Supports the upload (or import) of Green Button DMD files. Simply send
- * the DMD file within the POST data.
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {upload_access_token}]
- *
- * @param response
- * HTTP Servlet Response
- * @param retailCustomerId
- * The locally unique identifier of a Retail Customer - NOTE PII
- * @param params
- * HTTP Query Parameters
- * @param stream
- * An input stream
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- *
- * Usage:
- * POST /espi/1_1/resource/Batch/RetailCustomer/{retailCustomerId}/UsagePoint
- *
- */
- @PostMapping(value = "/Batch/RetailCustomer/{retailCustomerId}/UsagePoint",
- consumes = MediaType.APPLICATION_ATOM_XML_VALUE,
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Bulk Upload Green Button Data",
- description = "Uploads Green Button DMD (Download My Data) files for batch processing. " +
- "Supports bulk import of usage points, meter readings, and interval data."
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "201", description = "Bulk upload successful"),
- @ApiResponse(responseCode = "400", description = "Invalid ATOM XML or batch data"),
- @ApiResponse(responseCode = "401", description = "Unauthorized batch operation"),
- @ApiResponse(responseCode = "413", description = "File size exceeds limits")
- })
- public void upload(HttpServletResponse response,
- @Parameter(description = "Retail customer identifier", required = true)
- @PathVariable Long retailCustomerId,
- @RequestParam Map params, InputStream stream)
- throws IOException, FeedException {
-
- try {
- // retailCustomerService returns legacy RetailCustomer
- var retailCustomer = retailCustomerService
- .findById(retailCustomerId);
-
- // TODO: Implement ImportService
- // importService.importData(stream, retailCustomerId);
- throw new UnsupportedOperationException("ImportService not yet implemented");
-
- // do any notifications
-
- // TODO: Implement NotificationService
- // notificationService.notify(retailCustomer,
- // importService.getMinUpdated(),
- // importService.getMaxUpdated());
-
- } catch (Exception e) {
- System.out.printf("**** Batch Import Error: %s\n", e.toString());
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- /**
- * Supports Green Button Download My Data - A DMD file will be produced that
- * contains all Usage Points for the requested Retail Customer.
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {upload_access_token}]
- *
- * @param response
- * HTTP Servlet Response
- * @param retailCustomerId
- * The locally unique identifier of a Retail Customer - NOTE PII
- * @param params
- * HTTP Query Parameters
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- * Usage:
- * GET /espi/1_1/resource/Batch/RetailCustomer/{retailCustomerId}/UsagePoint
- *
- */
- @GetMapping(value = "/Batch/RetailCustomer/{retailCustomerId}/UsagePoint",
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Download Green Button Data Collection",
- description = "Downloads all usage points for a retail customer as Green Button DMD file"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "DMD file generated successfully"),
- @ApiResponse(responseCode = "400", description = "Invalid request parameters"),
- @ApiResponse(responseCode = "401", description = "Unauthorized access")
- })
- public void download_collection(HttpServletResponse response,
- @Parameter(description = "Retail customer identifier", required = true)
- @PathVariable Long retailCustomerId,
- @RequestParam Map params) throws IOException,
- FeedException {
-
- // Verify request contains valid query parameters
- if(!VerifyURLParams.verifyEntries("/espi/1_1/resource/Batch/RetailCustomer/{retailCustomerId}/UsagePoint", params)) {
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- response.addHeader("Content-Disposition",
- "attachment; filename=GreenButtonDownload.xml");
- try {
- // TODO -- need authorization hook
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- /**
- * Supports Green Button Download My Data A DMD file for a particular Usage
- * Point will be produced and returned to the Retail Customer
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {upload_access_token}]
- *
- * @param response
- * HTTP Servlet Response
- * @param retailCustomerId
- * The locally unique identifier of a Retail Customer - NOTE PII
- * @param usagePointId
- * The locally unique identifier of a UsagePointEntity.id
- * @param params
- * HTTP Query Parameters
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- *
- * Usage:
- * GET /espi/1_1/resource/Batch/RetailCustomer/{retailCustomerId}/UsagePoint/{usagePointId}
- *
- */
- @GetMapping(value = "/Batch/RetailCustomer/{retailCustomerId}/UsagePoint/{usagePointId}",
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Download Green Button Data Member",
- description = "Downloads specific usage point data for a retail customer as Green Button DMD file"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "DMD file generated successfully"),
- @ApiResponse(responseCode = "400", description = "Invalid request parameters"),
- @ApiResponse(responseCode = "401", description = "Unauthorized access"),
- @ApiResponse(responseCode = "404", description = "Usage point not found")
- })
- public void download_member(HttpServletResponse response,
- @Parameter(description = "Retail customer identifier", required = true)
- @PathVariable Long retailCustomerId,
- @Parameter(description = "Usage point identifier", required = true)
- @PathVariable Long usagePointId,
- @RequestParam Map params) throws IOException,
- FeedException {
-
- // Verify request contains valid query parameters
- if(!VerifyURLParams.verifyEntries("/espi/1_1/resource/Batch/RetailCustomer/{retailCustomerId}/UsagePoint/{usagePointId}", params)) {
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- response.addHeader("Content-Disposition",
- "attachment; filename=GreenButtonDownload.xml");
- try {
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- /**
- * Produce a SubscriptionEntity for the requester. The resultant response will
- * contain an Atom "feed" of the Usage Point(s) associated with the subscription.
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {access_token}]
- *
- * @param response
- * HTTP Servlet Response
- * @param subscriptionId
- * Long identifying the SubscriptionEntity.id of the desired
- * Authorization
- * @param params
- * HTTP Query Parameters
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- *
- * Usage:
- * GET /espi/1_1/resource/Batch/Subscription/{subscriptionId}
- *
- */
- @Transactional(readOnly = true)
- @GetMapping(value = "/Batch/Subscription/{subscriptionId}",
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Download SubscriptionEntity Data",
- description = "Downloads usage points associated with a subscription as Green Button feed"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Subscription data retrieved successfully"),
- @ApiResponse(responseCode = "400", description = "Invalid request parameters"),
- @ApiResponse(responseCode = "401", description = "Unauthorized access"),
- @ApiResponse(responseCode = "404", description = "Subscription not found")
- })
- public void subscription(HttpServletResponse response,
- @Parameter(description = "Subscription identifier", required = true)
- @PathVariable Long subscriptionId,
- @RequestParam Map params) throws IOException,
- FeedException {
-
- // Verify request contains valid query parameters
- if(!VerifyURLParams.verifyEntries("/espi/1_1/resource/Batch/Subscription/{subscriptionId}", params)) {
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- response.addHeader("Content-Disposition",
- "attachment; filename=GreenButtonDownload.xml");
- try {
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- /**
- * Produce a SubscriptionEntity for the requester. The resultant response will
- * contain an Atom "feed" of the Usage Point(s) associated with the subscription.
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {access_token}]
- *
- * @param response
- * HTTP Servlet Response
- * @param subscriptionId
- * Long identifying the SubscriptionEntity.id of the desired
- * Authorization
- * @param params
- * HTTP Query Parameters
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- *
- * Usage:
- * GET /espi/1_1/resource/Batch/Subscription/{subscriptionId}/UsagePoint
- *
- */
- @Transactional(readOnly = true)
- @GetMapping(value = "/Batch/Subscription/{subscriptionId}/UsagePoint",
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Download SubscriptionEntity Usage Points",
- description = "Downloads all usage points for a specific subscription as Green Button feed"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Usage point data retrieved successfully"),
- @ApiResponse(responseCode = "400", description = "Invalid request parameters"),
- @ApiResponse(responseCode = "401", description = "Unauthorized access"),
- @ApiResponse(responseCode = "404", description = "Subscription not found")
- })
- public void subscriptionUsagePoint(HttpServletResponse response,
- @Parameter(description = "Subscription identifier", required = true)
- @PathVariable Long subscriptionId,
- @RequestParam Map params) throws IOException,
- FeedException {
-
- // Verify request contains valid query parameters
- if(!VerifyURLParams.verifyEntries("/espi/1_1/resource/Batch/Subscription/{subscriptionId}/UsagePoint", params)) {
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- response.addHeader("Content-Disposition",
- "attachment; filename=GreenButtonDownload.xml");
- try {
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- /**
- * Produce a SubscriptionEntity for the requester. The resultant response will
- * contain an Atom "feed" of the Usage Point(s) associated with the subscription.
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {access_token}]
- *
- * @param response
- * HTTP Servlet Response
- * @param subscriptionId
- * Long identifying the SubscriptionEntity.id of the desired
- * Authorization
- * @param usagePointId
- * Long identifying the UsagePointEntity.id of the desired
- * Authorization
- * @param params
- * HTTP Query Parameters
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- *
- * Usage:
- * GET /espi/1_1/resource/Batch/Subscription/{subscriptionId}
- *
- */
-
- @Transactional(readOnly = true)
- @GetMapping(value = "/Batch/Subscription/{subscriptionId}/UsagePoint/{usagePointId}",
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Download Specific SubscriptionEntity Usage Point",
- description = "Downloads a specific usage point for a subscription as Green Button feed"
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Usage point data retrieved successfully"),
- @ApiResponse(responseCode = "400", description = "Invalid request parameters"),
- @ApiResponse(responseCode = "401", description = "Unauthorized access"),
- @ApiResponse(responseCode = "404", description = "Subscription or usage point not found")
- })
- public void subscriptionUsagePointMember(HttpServletResponse response,
- @Parameter(description = "Subscription identifier", required = true)
- @PathVariable Long subscriptionId,
- @Parameter(description = "Usage point identifier", required = true)
- @PathVariable Long usagePointId,
- @RequestParam Map params) throws IOException,
- FeedException {
-
- // Verify request contains valid query parameters
- if(!VerifyURLParams.verifyEntries("/espi/1_1/resource/Batch/Subscription/{subscriptionId}/UsagePoint/{usagePointId}", params)) {
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- response.addHeader("Content-Disposition",
- "attachment; filename=GreenButtonDownload.xml");
- try {
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- /**
- * Provide a Bulk delivery of information. The Third Party is provided with
- * the XML representation of the Bulk.
- *
- * RESTful Pattern: /espi/1_1/resource/Batch/Subscription/{subscriptionId}
- *
- * Requires Authorization: Bearer [{data_custodian_access_token} |
- * {client_access_token}]
- *
- * @param request
- * HTTP Servlet Request
- * @param response
- * HTTP Servlet Response
- * @param bulkId
- * Long identifying the BR={bulkId} within the
- * Authorization.scope string
- * @param params
- * HTTP Query Parameters
- * @throws IOException
- * Exception thrown by failed or interrupted I/O operations.
- * @throws FeedException
- * Exception thrown by WireFeedInput, WireFeedOutput, WireFeedParser
- * and WireFeedGenerator instances if they can not parse or generate a feed.
- *
- *
- * Usage:
- * GET /espi/1_1/resource/Batch/Bulk/{bulkId}
- *
- */
- @GetMapping(value = "/Batch/Bulk/{bulkId}",
- produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @Operation(
- summary = "Bulk Data Delivery",
- description = "Provides bulk delivery of information for third-party applications. " +
- "Supports both HTTPS and SFTP delivery methods."
- )
- @ApiResponses(value = {
- @ApiResponse(responseCode = "200", description = "Bulk data delivered successfully"),
- @ApiResponse(responseCode = "202", description = "Bulk data queued for SFTP delivery"),
- @ApiResponse(responseCode = "400", description = "Invalid request parameters"),
- @ApiResponse(responseCode = "401", description = "Unauthorized access")
- })
- // TODO Add support for wildcard bulkId parameters
- public void bulk(HttpServletRequest request, HttpServletResponse response,
- @Parameter(description = "Bulk identifier", required = true)
- @PathVariable Long bulkId,
- @RequestParam Map params)
- throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if(!VerifyURLParams.verifyEntries("/espi/1_1/resource/Batch/Bulk/{bulkId}", params)) {
-
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Request contains invalid query parameter values!");
- return;
- }
-
- // check to see if SFTP (or XML Caching) is turned on for this bulkId.
- //
- if (isSFTP(request)) {
- if (isInCache(request, bulkId)) {
- // TODO: Implement SFTP notification
- response.setStatus(HttpServletResponse.SC_ACCEPTED);
- return;
- } else {
- // build the file stream
- // in parallel
- // generate the xml to a file
- // return 302 response code
- // TODO: make SFTP cache location an configuration
- File destinationFile = new File(
- "/var/greenbutton/export-cache/" + bulkId);
- FileOutputStream fileOutputStream = new FileOutputStream(
- destinationFile);
-
- try {
- try {
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- if (destinationFile != null) {
- try {
- fileOutputStream.close();
- response.setStatus(HttpServletResponse.SC_ACCEPTED);
- } catch (IOException e) {
- // Ignore issues during closing
- }
- }
- }
-
- }
- } else {
- // not SFTP, use HTTPS as bulk response
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
- // note this default to a file just in case the handler doesn't want
- // to directly
- // parse the incoming stream.
-
- response.addHeader("Content-Disposition",
- "attachment; filename=GreenButtonDownload.xml");
-
- try {
- // exportService.export(...); // TODO: Implement
- response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
-
- }
-
- }
-
- private String getAuthorizationThirdParty(HttpServletRequest request) {
- String token = request.getHeader("authorization");
- String thirdParty = "";
-
- if (token != null) {
- token = token.replace("Bearer ", "");
- // authorizationService returns legacy Authorization
- var authorization = authorizationService
- .findByAccessToken(token);
- // authorization returns legacy ApplicationInformation
- var applicationInformation = authorization
- .getApplicationInformation();
- // note that ApplicationInformation.clientId is a String
- //
- thirdParty = applicationInformation.getClientId();
- }
-
- return thirdParty;
-
- }
-
- private boolean isInCache(HttpServletRequest request, Long bulkId) {
- System.out
- .println(System.getProperty("/var/greenbutton/export-cache/"));
- System.out.println(System.getProperty("/var/greenbutton/export-cache/"
- + bulkId));
- return false;
-
- }
-
- private boolean isSFTP(HttpServletRequest request) {
- Boolean result = false;
- String accessToken = request.getHeader("authorization");
- if (accessToken != null) {
- if (accessToken.contains("Bearer")) {
- // has AuthorizationEntity header with Bearer type
- accessToken = accessToken.replace("Bearer ", "");
- // ensure length is >12 characters (48 bits in hex at least)
- if (accessToken.length() >= 12) {
- // we have a valid token
- AuthorizationEntity authorization = authorizationService
- .findByAccessToken(accessToken);
- ApplicationInformationEntity applicationInformation = authorization
- .getApplicationInformation();
- String bulkRequestUri = applicationInformation
- .getDataCustodianBulkRequestURI();
- if (bulkRequestUri.contains("sftp:")) {
- result = true;
- }
- }
- }
- }
- return result;
- }
-
-
-}
diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java
index cd1b8b5a..907b0f3d 100644
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java
+++ b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java
@@ -27,11 +27,9 @@
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
-import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity;
import org.greenbuttonalliance.espi.common.dto.customer.CustomerAccountDto;
import org.greenbuttonalliance.espi.common.mapper.customer.CustomerAccountMapper;
import org.greenbuttonalliance.espi.common.repositories.customer.CustomerAccountRepository;
-import org.greenbuttonalliance.espi.common.service.customer.CustomerAccountService;
import org.greenbuttonalliance.espi.common.service.impl.CustomerAccountExportService;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
@@ -39,19 +37,18 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
-import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.io.ByteArrayOutputStream;
-import java.net.URI;
import java.util.List;
import java.util.UUID;
/**
- * Modern RESTful controller for managing CustomerAccount resources according to the
+ * Modern RESTful controller for reading CustomerAccount resources according to the
* Green Button Alliance ESPI (Energy Services Provider Interface) specification.
*
- * This controller handles CustomerAccount operations with modern Spring Boot 3.5 patterns,
- * returning XML output via JAXB-marshalled responses.
+ * GET-only, returning XML via JAXB-marshalled responses. The CRUD write endpoints
+ * (POST/PUT/DELETE) are deferred — they are admin/sandbox-DB management APIs to be delivered
+ * in the separate admin-CRUD track (see issue #119 build plan).
*/
@RestController
@RequestMapping("/espi/1_1/resource")
@@ -63,7 +60,6 @@ public class CustomerAccountRESTController {
private final CustomerAccountRepository customerAccountRepository;
private final CustomerAccountMapper customerAccountMapper;
private final CustomerAccountExportService customerAccountExportService;
- private final CustomerAccountService customerAccountService;
/**
* Get all Customer Accounts (root collection).
@@ -125,83 +121,4 @@ public ResponseEntity getCustomerAccount(
.contentType(MediaType.APPLICATION_XML)
.body(out.toByteArray());
}
-
- /**
- * Create a new Customer Account.
- */
- @PostMapping(value = "/CustomerAccount", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
- @Operation(
- summary = "Create CustomerAccount",
- description = "Creates a new CustomerAccount resource.",
- responses = {
- @ApiResponse(responseCode = "201", description = "Successfully created CustomerAccount"),
- @ApiResponse(responseCode = "400", description = "Invalid data"),
- @ApiResponse(responseCode = "401", description = "Unauthorized"),
- @ApiResponse(responseCode = "403", description = "Forbidden - insufficient scope")
- }
- )
- public ResponseEntity createCustomerAccount(@RequestBody CustomerAccountDto dto) {
- CustomerAccountEntity entity = customerAccountMapper.toEntity(dto);
- CustomerAccountEntity savedEntity = customerAccountService.save(entity);
- CustomerAccountDto savedDto = customerAccountMapper.toDto(savedEntity);
-
- URI location = ServletUriComponentsBuilder.fromCurrentRequest()
- .path("/{id}")
- .buildAndExpand(savedEntity.getId())
- .toUri();
-
- return ResponseEntity.created(location).body(savedDto);
- }
-
- /**
- * Update an existing Customer Account.
- */
- @PutMapping(value = "/CustomerAccount/{customerAccountId}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
- @Operation(
- summary = "Update CustomerAccount",
- description = "Updates an existing CustomerAccount resource.",
- responses = {
- @ApiResponse(responseCode = "200", description = "Successfully updated CustomerAccount"),
- @ApiResponse(responseCode = "404", description = "CustomerAccount not found"),
- @ApiResponse(responseCode = "400", description = "Invalid data"),
- @ApiResponse(responseCode = "401", description = "Unauthorized"),
- @ApiResponse(responseCode = "403", description = "Forbidden - insufficient scope")
- }
- )
- public ResponseEntity updateCustomerAccount(
- @PathVariable UUID customerAccountId,
- @RequestBody CustomerAccountDto dto) {
-
- if (!customerAccountRepository.existsById(customerAccountId)) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "CustomerAccount not found for id: " + customerAccountId);
- }
-
- CustomerAccountEntity entity = customerAccountMapper.toEntity(dto);
- entity.setId(customerAccountId);
- CustomerAccountEntity updatedEntity = customerAccountService.save(entity);
-
- return ResponseEntity.ok(customerAccountMapper.toDto(updatedEntity));
- }
-
- /**
- * Delete a Customer Account.
- */
- @DeleteMapping("/CustomerAccount/{customerAccountId}")
- @Operation(
- summary = "Delete CustomerAccount",
- description = "Deletes an existing CustomerAccount resource.",
- responses = {
- @ApiResponse(responseCode = "204", description = "Successfully deleted CustomerAccount"),
- @ApiResponse(responseCode = "404", description = "CustomerAccount not found"),
- @ApiResponse(responseCode = "401", description = "Unauthorized"),
- @ApiResponse(responseCode = "403", description = "Forbidden - insufficient scope")
- }
- )
- public ResponseEntity deleteCustomerAccount(@PathVariable UUID customerAccountId) {
- if (!customerAccountRepository.existsById(customerAccountId)) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "CustomerAccount not found for id: " + customerAccountId);
- }
- customerAccountRepository.deleteById(customerAccountId);
- return ResponseEntity.noContent().build();
- }
-}
\ No newline at end of file
+}
diff --git a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java.disabled b/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java.disabled
deleted file mode 100644
index 94a3ea77..00000000
--- a/openespi-datacustodian/src/main/java/org/greenbuttonalliance/espi/datacustodian/web/api/CustomerAccountRESTController.java.disabled
+++ /dev/null
@@ -1,542 +0,0 @@
-/*
- *
- * Copyright (c) 2025 Green Button Alliance, Inc.
- *
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.greenbuttonalliance.espi.datacustodian.web.api;
-
-import com.sun.syndication.io.FeedException;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.security.SecurityRequirement;
-import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerAccountEntity;
-import org.greenbuttonalliance.espi.common.dto.customer.CustomerAccountDto;
-import org.greenbuttonalliance.espi.common.service.customer.CustomerAccountService;
-import org.greenbuttonalliance.espi.common.service.DtoExportService;
-import org.greenbuttonalliance.espi.datacustodian.utils.VerifyURLParams;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.Valid;
-import java.io.IOException;
-import java.math.BigDecimal;
-import java.util.Map;
-import java.util.UUID;
-
-/**
- * RESTful controller for managing Customer Account resources according to the
- * Green Button Alliance ESPI (Energy Services Provider Interface) specification.
- *
- * CustomerAccount represents a billing account relationship between a customer and
- * utility company, including account status, billing preferences, payment history,
- * and financial arrangements.
- *
- * Features:
- * - Account lifecycle management (active, suspended, closed)
- * - Billing and payment tracking
- * - Service agreements and rate schedules
- * - Account hierarchy for commercial customers
- * - Financial reporting and statements
- */
-// @RestController - COMMENTED OUT: Legacy controller disabled for simplification
-// @Component
-// @RequestMapping - DISABLED("/espi/1_1/resource")
-@Tag(name = "Customer Account", description = "Customer Billing Account Management API")
-@SecurityRequirement(name = "OAuth2")
-public class CustomerAccountRESTController {
-
- private final CustomerAccountService customerAccountService;
- private final DtoExportService exportService;
-
- @Autowired
- public CustomerAccountRESTController(
- CustomerAccountService customerAccountService,
- DtoExportService exportService) {
- this.customerAccountService = customerAccountService;
- this.exportService = exportService;
- }
-
- @ExceptionHandler(Exception.class)
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- public void handleGenericException() {
- // Generic exception handler
- }
-
- // ================================
- // Customer Account Collection APIs
- // ================================
-
- /**
- * Retrieves all CustomerAccount resources.
- *
- * @param request HTTP servlet request for authorization context
- * @param response HTTP response for streaming ATOM XML content
- * @param params Query parameters for filtering and pagination
- * @throws IOException if output stream cannot be written
- * @throws FeedException if ATOM feed generation fails
- */
- @GetMapping(value = "/CustomerAccount", produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @PreAuthorize("hasAuthority('SCOPE_ACCOUNT_READ')")
- @Operation(
- summary = "Get CustomerAccount Collection",
- description = "Retrieves all customer billing accounts with optional filtering and pagination. " +
- "Returns an ATOM feed containing CustomerAccount entries including account status, " +
- "billing information, and service relationships.",
- security = {@SecurityRequirement(name = "OAuth2", scopes = {"ACCOUNT_READ"})}
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Successfully retrieved CustomerAccount collection",
- content = @Content(mediaType = MediaType.APPLICATION_ATOM_XML_VALUE,
- schema = @Schema(description = "ATOM feed containing CustomerAccount entries"))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "Invalid query parameters provided"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized access to account resources"
- )
- })
- public void getCustomerAccountCollection(
- HttpServletRequest request,
- HttpServletResponse response,
- @Parameter(description = "Query parameters for filtering (published-max, published-min, updated-max, updated-min, max-results, start-index)")
- @RequestParam Map params) throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if (!VerifyURLParams.verifyEntries("/espi/1_1/resource/CustomerAccount", params)) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST,
- "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
-
- try {
- exportService.exportCustomerAccounts(response.getOutputStream(),
- params);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- /**
- * Retrieves a specific CustomerAccount by ID.
- *
- * @param customerAccountId Unique identifier for the CustomerAccount
- * @param request HTTP servlet request for authorization context
- * @param response HTTP response for streaming ATOM XML content
- * @param params Query parameters for export filtering
- * @throws IOException if output stream cannot be written
- * @throws FeedException if ATOM entry generation fails
- */
- @GetMapping(value = "/CustomerAccount/{customerAccountId}", produces = MediaType.APPLICATION_ATOM_XML_VALUE)
- @PreAuthorize("hasAuthority('SCOPE_ACCOUNT_READ') and @accountSecurityService.hasAccessToAccount(authentication, #customerAccountId)")
- @Operation(
- summary = "Get CustomerAccount by ID",
- description = "Retrieves a specific customer billing account by its unique identifier. " +
- "Returns an ATOM entry containing the CustomerAccount details including " +
- "account status, billing cycle, payment methods, and service agreements.",
- security = {@SecurityRequirement(name = "OAuth2", scopes = {"ACCOUNT_READ"})}
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Successfully retrieved CustomerAccount",
- content = @Content(mediaType = MediaType.APPLICATION_ATOM_XML_VALUE,
- schema = @Schema(description = "ATOM entry containing CustomerAccount details"))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "Invalid customerAccountId or query parameters"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized access to this account"
- ),
- @ApiResponse(
- responseCode = "404",
- description = "CustomerAccount not found"
- )
- })
- public void getCustomerAccount(
- @Parameter(description = "Unique identifier of the CustomerAccount", required = true)
- @PathVariable UUID customerAccountId,
- HttpServletRequest request,
- HttpServletResponse response,
- @Parameter(description = "Query parameters for export filtering")
- @RequestParam Map params) throws IOException, FeedException {
-
- // Verify request contains valid query parameters
- if (!VerifyURLParams.verifyEntries("/espi/1_1/resource/CustomerAccount/{customerAccountId}", params)) {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST,
- "Request contains invalid query parameter values!");
- return;
- }
-
- response.setContentType(MediaType.APPLICATION_ATOM_XML_VALUE);
-
- try {
- exportService.exportCustomerAccount(customerAccountId, response.getOutputStream(),
- params);
- } catch (Exception e) {
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- }
- }
-
- /**
- * Creates a new CustomerAccount resource.
- *
- * @param customerAccountDto CustomerAccount data
- * @param request HTTP servlet request for authorization context
- * @return Created CustomerAccount response
- */
- @PostMapping(value = "/CustomerAccount",
- consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_ATOM_XML_VALUE},
- produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_ATOM_XML_VALUE})
- @PreAuthorize("hasAuthority('SCOPE_ACCOUNT_WRITE') and hasRole('DATA_CUSTODIAN')")
- @Operation(
- summary = "Create CustomerAccount",
- description = "Creates a new customer billing account. Establishes the relationship " +
- "between a customer and utility service including billing preferences, " +
- "payment methods, and service level agreements.",
- security = {@SecurityRequirement(name = "OAuth2", scopes = {"ACCOUNT_WRITE"})}
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "201",
- description = "Successfully created CustomerAccount",
- content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
- schema = @Schema(implementation = CustomerAccountDto.class))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "Invalid CustomerAccount data"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized to create accounts"
- )
- })
- public ResponseEntity createCustomerAccount(
- @Parameter(description = "CustomerAccount data", required = true)
- @Valid @RequestBody CustomerAccountDto customerAccountDto,
- HttpServletRequest request) {
-
- try {
- CustomerAccountEntity customerAccount = customerAccountService.createCustomerAccount(customerAccountDto);
- CustomerAccountDto responseDto = customerAccountService.toDto(customerAccount);
- return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
- } catch (Exception e) {
- return ResponseEntity.badRequest().build();
- }
- }
-
- /**
- * Updates an existing CustomerAccount resource.
- *
- * @param customerAccountId Unique identifier for the CustomerAccount to update
- * @param customerAccountDto Updated CustomerAccount data
- * @param request HTTP servlet request for authorization context
- * @return Updated CustomerAccount response
- */
- @PutMapping(value = "/CustomerAccount/{customerAccountId}",
- consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_ATOM_XML_VALUE},
- produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_ATOM_XML_VALUE})
- @PreAuthorize("hasAuthority('SCOPE_ACCOUNT_WRITE') and @accountSecurityService.hasAccessToAccount(authentication, #customerAccountId)")
- @Operation(
- summary = "Update CustomerAccount",
- description = "Updates an existing customer billing account. Allows modification of " +
- "account status, billing preferences, payment methods, and service configurations.",
- security = {@SecurityRequirement(name = "OAuth2", scopes = {"ACCOUNT_WRITE"})}
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Successfully updated CustomerAccount",
- content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
- schema = @Schema(implementation = CustomerAccountDto.class))
- ),
- @ApiResponse(
- responseCode = "400",
- description = "Invalid CustomerAccount data"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized to update this account"
- ),
- @ApiResponse(
- responseCode = "404",
- description = "CustomerAccount not found"
- )
- })
- public ResponseEntity updateCustomerAccount(
- @Parameter(description = "Unique identifier of the CustomerAccount to update", required = true)
- @PathVariable UUID customerAccountId,
- @Parameter(description = "Updated CustomerAccount data", required = true)
- @Valid @RequestBody CustomerAccountDto customerAccountDto,
- HttpServletRequest request) {
-
- try {
- CustomerAccountEntity customerAccount = customerAccountService.updateCustomerAccount(customerAccountId, customerAccountDto);
- CustomerAccountDto responseDto = customerAccountService.toDto(customerAccount);
- return ResponseEntity.ok(responseDto);
- } catch (Exception e) {
- return ResponseEntity.badRequest().build();
- }
- }
-
- /**
- * Deletes a CustomerAccount resource.
- *
- * @param customerAccountId Unique identifier for the CustomerAccount to delete
- * @param request HTTP servlet request for authorization context
- * @return Deletion confirmation response
- */
- @DeleteMapping("/CustomerAccount/{customerAccountId}")
- @PreAuthorize("hasAuthority('SCOPE_ACCOUNT_DELETE') and hasRole('DATA_CUSTODIAN')")
- @Operation(
- summary = "Delete CustomerAccount",
- description = "Removes a customer billing account. This will close the account, " +
- "finalize billing, and archive transaction history while maintaining " +
- "regulatory compliance for record retention.",
- security = {@SecurityRequirement(name = "OAuth2", scopes = {"ACCOUNT_DELETE"})}
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "204",
- description = "Successfully deleted CustomerAccount"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized to delete accounts"
- ),
- @ApiResponse(
- responseCode = "404",
- description = "CustomerAccount not found"
- )
- })
- public ResponseEntity deleteCustomerAccount(
- @Parameter(description = "Unique identifier of the CustomerAccount to delete", required = true)
- @PathVariable UUID customerAccountId,
- HttpServletRequest request) {
-
- try {
- customerAccountService.deleteCustomerAccount(customerAccountId);
- return ResponseEntity.noContent().build();
- } catch (Exception e) {
- return ResponseEntity.notFound().build();
- }
- }
-
- // ============================================
- // Customer Account Financial Management APIs
- // ============================================
-
- /**
- * Retrieves the current account balance and billing status.
- *
- * @param customerAccountId Unique identifier for the CustomerAccount
- * @param request HTTP servlet request for authorization context
- * @return Account balance and billing information
- */
- @GetMapping(value = "/CustomerAccount/{customerAccountId}/balance", produces = MediaType.APPLICATION_JSON_VALUE)
- @PreAuthorize("hasAuthority('SCOPE_ACCOUNT_READ') and @accountSecurityService.hasAccessToAccount(authentication, #customerAccountId)")
- @Operation(
- summary = "Get Account Balance",
- description = "Retrieves current account balance, billing status, payment due dates, " +
- "and outstanding charges for the specified customer account.",
- security = {@SecurityRequirement(name = "OAuth2", scopes = {"ACCOUNT_READ"})}
- )
- @ApiResponses(value = {
- @ApiResponse(
- responseCode = "200",
- description = "Successfully retrieved account balance"
- ),
- @ApiResponse(
- responseCode = "401",
- description = "Unauthorized access to account financial data"
- ),
- @ApiResponse(
- responseCode = "404",
- description = "CustomerAccount not found"
- )
- })
- public ResponseEntity