From 965ab5b4f1fa61f6117934485d027f614fb7df63 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 2 Oct 2026 14:45:31 -0700 Subject: [PATCH] Updated location group patch with query parameter to allow for metadata replacement. Uses existing DB procedures. Integration test included. --- .../main/java/cwms/cda/api/Controllers.java | 1 + .../cwms/cda/api/LocationGroupController.java | 11 ++- .../cwms/cda/data/dao/LocationGroupDao.java | 51 +++++++++- .../api/LocationGroupControllerTestIT.java | 99 +++++++++++++++++++ 4 files changed, 158 insertions(+), 4 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index 02fbbcc58..acd135fa1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -173,6 +173,7 @@ public final class Controllers { public static final String GROUP_ID = "group-id"; public static final String REPLACE_ASSIGNED_LOCS = "replace-assigned-locs"; + public static final String REPLACE_METADATA = "replace-metadata"; public static final String REPLACE_ASSIGNED_TS = "replace-assigned-ts"; public static final String TS_IDS = "ts-ids"; public static final String IGNORE_MISSING = "ignore-missing"; diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java index 1f0185c8c..aea2012c0 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java @@ -97,7 +97,8 @@ private Timer.Context markAndTime(String subject) { + "owning office of the category the location group belongs to " + "whose data is to be included in the response."), @OpenApiParam(name = LOCATION_OFFICE_ID, description = "Specifies the " - + "owning office of the location assigned to the location group whose data is to be included in the response."), + + "owning office of the location assigned to the location group whose data is " + + "to be included in the response."), }, responses = { @OpenApiResponse(status = STATUS_200, @@ -313,6 +314,8 @@ public void create(@NotNull Context ctx) { @OpenApiParam(name = REPLACE_ASSIGNED_LOCS, type = Boolean.class, description = "Specifies whether to " + "unassign all existing locations before assigning new locations specified in the content body " + "Default: false"), + @OpenApiParam(name = REPLACE_METADATA, type = Boolean.class, description = "Specifies whether to " + + "replace group metadata with that specified in the content body. Default: false"), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "office of the user making the request. This is the office that the location, group, and category " + "belong to. If the group and/or category belong to the CWMS office, " @@ -335,7 +338,7 @@ public void update(@NotNull Context ctx, @NotNull String groupId) { LocationGroup deserialize = Formats.parseContent(contentType, body, LocationGroup.class); boolean replaceAssignedLocs = ctx.queryParamAsClass(REPLACE_ASSIGNED_LOCS, Boolean.class).getOrDefault(false); - boolean ignoreMissing = ctx.queryParamAsClass(IGNORE_MISSING, Boolean.class).getOrDefault(false); + boolean replaceMetadata = ctx.queryParamAsClass(REPLACE_METADATA, Boolean.class).getOrDefault(false); LocationGroupDao locationGroupDao = new LocationGroupDao(dsl); if (!office.equalsIgnoreCase(CWMS_OFFICE) && !groupId.equals(deserialize.getId())) { locationGroupDao.renameLocationGroup(groupId, deserialize); @@ -343,6 +346,10 @@ public void update(@NotNull Context ctx, @NotNull String groupId) { if (replaceAssignedLocs) { locationGroupDao.unassignAllLocs(deserialize, office); } + if (replaceMetadata) { + locationGroupDao.replaceWithMetadata(deserialize, replaceAssignedLocs); + } + boolean ignoreMissing = ctx.queryParamAsClass(IGNORE_MISSING, Boolean.class).getOrDefault(false); List missingLocations = locationGroupDao.assignLocs(deserialize, office, ignoreMissing); if (missingLocations.isEmpty()) { ctx.status(HttpServletResponse.SC_OK); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java index 064e3841f..88d89d82d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java @@ -29,6 +29,7 @@ import static org.jooq.impl.DSL.count; import static org.jooq.impl.DSL.noCondition; +import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.AssignedLocation; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.LocationCategory; @@ -94,7 +95,19 @@ public LocationGroupDao(DSLContext dsl) { * @return An optional location group. */ public Optional getLocationGroup(@NotNull String officeId, @NotNull String categoryId, - @NotNull String groupId) { + @NotNull String groupId) { + return getLocationGroup(dsl, officeId, categoryId, groupId); + } + + /** + * Get a location group by office, category, and group id. + * @param officeId The office id to use for the query. + * @param categoryId The category id to use for the query. + * @param groupId The group id to use for the query. + * @return An optional location group. + */ + public Optional getLocationGroup(@NotNull DSLContext context, @NotNull String officeId, + @NotNull String categoryId, @NotNull String groupId) { officeId = officeId.toUpperCase(); Condition joinCondition; @@ -109,7 +122,7 @@ public Optional getLocationGroup(@NotNull String officeId, @NotNu .and(catGroupView.GRP_DB_OFFICE_ID.in(CWMS, officeId)) .and(catGroupView.CAT_DB_OFFICE_ID.in(CWMS, officeId)); - LocationGroup locGroup = buildQuery(whereCondition, joinCondition) + LocationGroup locGroup = buildQuery(whereCondition, joinCondition, context) .fetchSize(DEFAULT_FETCH_SIZE) .fetchOne(mapToLocationGroup); @@ -509,6 +522,40 @@ public List create(LocationGroup group, boolean allowPartialAssignment) }); } + /** + * Replace a stored location group with a new group, storing the new group's metadata + * @param group The new group to replace the old one with + * @param replaceAssignedLocs If true, replace the assigned locations with the new group's assigned locations + */ + public void replaceWithMetadata(LocationGroup group, boolean replaceAssignedLocs) { + String office = group.getOfficeId(); + String categoryId = group.getLocationCategory().getId(); + String groupId = group.getId(); + connection(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + dslContext.transaction((Configuration trx) -> { + DSLContext transactionContext = trx.dsl(); + Optional oldGroup = getLocationGroup(transactionContext, office, categoryId, groupId); + if (oldGroup.isPresent()) { + Configuration config = transactionContext.configuration(); + CWMS_LOC_PACKAGE.call_UNASSIGN_LOC_GROUP(config, + categoryId, groupId, null, "T", office); + CWMS_LOC_PACKAGE.call_DELETE_LOC_GROUP__2(config, categoryId, + groupId, "F", office); + LocationGroup newGroup = new LocationGroup(group, oldGroup.get().getAssignedLocations()); + CWMS_LOC_PACKAGE.call_CREATE_LOC_GROUP2(config, categoryId, + newGroup.getId(), newGroup.getDescription(), newGroup.getOfficeId(), + newGroup.getSharedLocAliasId(), newGroup.getSharedRefLocationId()); + if (!replaceAssignedLocs) { + assignLocs(trx, newGroup, office); + } + } else { + throw new NotFoundException("Location Group " + categoryId + "/" + groupId + " not found"); + } + }); + }); + } + @NotNull private static LOC_ALIAS_TYPE3 convertToLocAliasType(AssignedLocation a) { BigDecimal attribute = toBigDecimal(a.getAttribute()); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java index 4d7d92ae1..ab5d019a1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java @@ -1230,6 +1230,105 @@ void test_rename_group(String format) throws Exception { .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_update_group_metadata(String format) throws Exception { + // validate that we can update the group aliasID, description, and loc ref + String officeId = user.getOperatingOffice(); + String locationId = "LocGroupMetaTest"; + createLocation(locationId, true, officeId); + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, "AliasId", 1, locationId); + LocationCategory cat = new LocationCategory(officeId, "test_metadata_group", "IntegrationTesting"); + LocationGroup group = new LocationGroup(new LocationGroup(cat, officeId, "test_rename_group", null, + null, null, null), Collections.singletonList(assignLoc)); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); + ContentType contentType = Formats.parseHeader(Formats.JSON, LocationCategory.class); + String categoryXml = Formats.format(contentType, cat); + String groupXml = Formats.format(contentType, group); + //Create Category + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(categoryXml) + .header("Authorization", user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/category/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + //Create Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + String sharedLocAliasId = "sharedLocAliasId"; + LocationGroup newGroup = new LocationGroup(cat, officeId, "test_metadata_group_new", "IntegrationTesting", + sharedLocAliasId, locationId, 123); + groupsToCleanup.add(newGroup); + String newGroupXml = Formats.format(contentType, newGroup); + //Rename Group, including metadata + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(newGroupXml) + .header("Authorization", user.toHeaderValue()) + .header(CATEGORY_ID, group.getLocationCategory().getId()) + .queryParam(REPLACE_METADATA, true) + .queryParam(OFFICE, group.getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/group/"+ group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + //Read + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, group.getLocationCategory().getId()) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + newGroup.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(newGroup.getOfficeId())) + .body("id", equalTo(newGroup.getId())) + .body("description", equalTo(newGroup.getDescription())) + .body("location-category.office-id", equalTo(officeId)) + .body("location-category.id", equalTo(group.getLocationCategory().getId())) + .body("location-category.description", equalTo(group.getLocationCategory().getDescription())) + .body("shared-loc-alias-id", equalTo(sharedLocAliasId)) + .body("shared-ref-location-id", equalTo(locationId)) + .body("assigned-locations[0].location-id", equalTo(locationId)) + .body("assigned-locations[0].alias-id", equalTo("AliasId")) + .body("assigned-locations[0].ref-location-id", equalTo(locationId)); + } + @ParameterizedTest @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) void test_add_assigned_locs(String format) throws Exception {