From f24f15ccdaa3644add508f1b228ad2e86f47d997 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Thu, 16 Jul 2026 15:59:07 -0700 Subject: [PATCH 1/5] Initial changes to support partial group storage. Includes location group integration tests. Updates to latest JOOQ codegen --- .../main/java/cwms/cda/api/Controllers.java | 7 +- .../cwms/cda/api/LocationGroupController.java | 68 +++++++---- .../cda/api/TimeSeriesGroupController.java | 48 +++++++- .../cda/api/errors/NotFoundException.java | 6 + .../main/java/cwms/cda/data/dao/JooqDao.java | 17 ++- .../cwms/cda/data/dao/LocationGroupDao.java | 61 ++++++++-- .../cwms/cda/data/dao/TimeSeriesGroupDao.java | 73 +++++++----- .../api/LocationGroupControllerTestIT.java | 106 ++++++++++++++++++ .../location_group_missing_assignments.json | 19 ++++ gradle/libs.versions.toml | 2 +- 10 files changed, 341 insertions(+), 66 deletions(-) create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/location_group_missing_assignments.json 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 ed05a37267..9d45bbd90f 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 @@ -40,9 +40,7 @@ import io.javalin.core.validation.Validator; import io.javalin.http.Context; import java.time.Instant; -import java.time.ZoneId; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -174,10 +172,11 @@ public final class Controllers { public static final String REPLACE_ASSIGNED_LOCS = "replace-assigned-locs"; 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"; public static final String EXAMPLE_DATE = "2021-06-10T13:00:00-07:00"; - public static final String TIME_FORMAT_DESC = "The format for this field " + - "is ISO 8601 extended in UTC, e.g., 2026-06-18T19:42:00Z"; + public static final String TIME_FORMAT_DESC = "The format for this field " + + "is ISO 8601 extended in UTC, e.g., 2026-06-18T19:42:00Z"; public static final String INCLUDE_ASSIGNED = "include-assigned"; public static final String ANY_MASK = "*"; 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 70bde18bb1..1f0185c8ca 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 @@ -25,23 +25,7 @@ package cwms.cda.api; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.CASCADE_DELETE; -import static cwms.cda.api.Controllers.CATEGORY_ID; -import static cwms.cda.api.Controllers.CATEGORY_OFFICE_ID; -import static cwms.cda.api.Controllers.CREATE; -import static cwms.cda.api.Controllers.CWMS_OFFICE; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.GET_ONE; -import static cwms.cda.api.Controllers.GROUP_ID; -import static cwms.cda.api.Controllers.GROUP_OFFICE_ID; -import static cwms.cda.api.Controllers.INCLUDE_ASSIGNED; -import static cwms.cda.api.Controllers.LOCATION_CATEGORY_LIKE; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.REPLACE_ASSIGNED_LOCS; -import static cwms.cda.api.Controllers.RESULTS; -import static cwms.cda.api.Controllers.SIZE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.UPDATE; +import static cwms.cda.api.Controllers.*; import static cwms.cda.api.Controllers.queryParamAsClass; import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; @@ -54,6 +38,7 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.LocationGroupDao; +import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.LocationGroup; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; @@ -61,6 +46,7 @@ import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; +import io.javalin.http.HttpCode; import io.javalin.plugin.openapi.annotations.HttpMethod; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; @@ -68,7 +54,9 @@ import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import javax.servlet.http.HttpServletResponse; import org.geojson.FeatureCollection; @@ -260,6 +248,10 @@ public void getOne(@NotNull Context ctx, @NotNull String groupId) { @OpenApiContent(from = LocationGroup.class, type = Formats.JSON) }, required = true), + queryParams = { + @OpenApiParam(name = IGNORE_MISSING, description = "Specifies whether to fail when attempting " + + "to assign a location that does not exist. Default is false.", type = Boolean.class) + }, method = HttpMethod.POST, tags = {TAG} ) @@ -272,6 +264,7 @@ public void create(@NotNull Context ctx) { String body = ctx.body(); ContentType contentType = Formats.parseHeader(formatHeader, LocationGroup.class); LocationGroup deserialize = Formats.parseContent(contentType, body, LocationGroup.class); + boolean ignoreMissing = ctx.queryParamAsClass(IGNORE_MISSING, Boolean.class).getOrDefault(false); if (!deserialize.getLocationCategory().getOfficeId().equalsIgnoreCase(CWMS_OFFICE) && (!deserialize.getOfficeId().equalsIgnoreCase(deserialize.getLocationCategory().getOfficeId()) @@ -281,8 +274,26 @@ public void create(@NotNull Context ctx) { } LocationGroupDao dao = new LocationGroupDao(dsl); - dao.create(deserialize); - ctx.status(HttpServletResponse.SC_CREATED); + List missingLocations = dao.create(deserialize, ignoreMissing); + if (missingLocations.isEmpty()) { + ctx.status(HttpServletResponse.SC_CREATED); + } else { + Map details = new HashMap<>(); + StringBuilder sb = new StringBuilder(); + for (CwmsId missingLocation : missingLocations) { + sb.append(missingLocation.getName()); + sb.append(", "); + } + sb.delete(sb.length() - 2, sb.length()); + details.put("missing-locations", sb.toString()); + if (ignoreMissing) { + ctx.status(HttpCode.MULTI_STATUS); + } else { + ctx.status(HttpServletResponse.SC_BAD_REQUEST); + details.put("message", "One or more locations could not be assigned to the location group."); + } + ctx.json(details); + } } } @@ -306,6 +317,8 @@ public void create(@NotNull Context ctx) { + "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, " + "this only identifies the location."), + @OpenApiParam(name = IGNORE_MISSING, description = "Specifies whether to fail when attempting " + + "to assign a location that does not exist. Default is false.", type = Boolean.class) }, method = HttpMethod.PATCH, tags = {TAG} @@ -322,6 +335,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); LocationGroupDao locationGroupDao = new LocationGroupDao(dsl); if (!office.equalsIgnoreCase(CWMS_OFFICE) && !groupId.equals(deserialize.getId())) { locationGroupDao.renameLocationGroup(groupId, deserialize); @@ -329,8 +343,20 @@ public void update(@NotNull Context ctx, @NotNull String groupId) { if (replaceAssignedLocs) { locationGroupDao.unassignAllLocs(deserialize, office); } - locationGroupDao.assignLocs(deserialize, office); - ctx.status(HttpServletResponse.SC_OK); + List missingLocations = locationGroupDao.assignLocs(deserialize, office, ignoreMissing); + if (missingLocations.isEmpty()) { + ctx.status(HttpServletResponse.SC_OK); + } else { + Map details = new HashMap<>(); + details.put("missing_locations", Formats.format(contentType, missingLocations, CwmsId.class)); + if (ignoreMissing) { + ctx.status(HttpCode.MULTI_STATUS); + } else { + ctx.status(HttpServletResponse.SC_BAD_REQUEST); + details.put("message", "One or more locations could not be assigned to the location group."); + } + ctx.json(details); + } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java index 2339c3a4c4..6355032a79 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java @@ -35,6 +35,7 @@ import static cwms.cda.api.Controllers.GET_ONE; import static cwms.cda.api.Controllers.GROUP_ID; import static cwms.cda.api.Controllers.GROUP_OFFICE_ID; +import static cwms.cda.api.Controllers.IGNORE_MISSING; import static cwms.cda.api.Controllers.IGNORE_NULLS; import static cwms.cda.api.Controllers.INCLUDE_ASSIGNED; import static cwms.cda.api.Controllers.OFFICE; @@ -57,6 +58,8 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.TimeSeriesGroupDao; +import cwms.cda.data.dto.AssignedTimeSeries; +import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.TimeSeriesGroup; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; @@ -71,7 +74,9 @@ import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -243,6 +248,8 @@ public void getOne(@NotNull Context ctx, @NotNull String groupId) { queryParams = { @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, description = "Create will fail if provided ID already exists. Default: true"), + @OpenApiParam(name = IGNORE_MISSING, type = Boolean.class, description = "If true, do not fail when " + + "attempting to assign a time series that does not exist to the group"), @OpenApiParam(name = IGNORE_NULLS, type = Boolean.class, description = "Ignore null values in the request body. Caution, if " + FAIL_IF_EXISTS + " is false and " + IGNORE_NULLS + " is false, then the create will proceed whether " @@ -276,9 +283,26 @@ public void create(@NotNull Context ctx) { boolean ignoreNulls = ctx.queryParamAsClass(IGNORE_NULLS, Boolean.class).getOrDefault(true); boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true); + boolean ignoreMissing = ctx.queryParamAsClass(IGNORE_MISSING, Boolean.class).getOrDefault(false); TimeSeriesGroupDao dao = new TimeSeriesGroupDao(dsl); - dao.create(deserialize, failIfExists, ignoreNulls); - ctx.status(HttpServletResponse.SC_CREATED); + List missingTimeSeries = dao.create(deserialize, failIfExists, ignoreNulls, ignoreMissing); + if (missingTimeSeries.isEmpty()) { + ctx.status(HttpServletResponse.SC_CREATED); + } else { + Map detailsMap = new HashMap<>(); + detailsMap.put("missing-timeseries", Formats.format(contentType, missingTimeSeries, + AssignedTimeSeries.class)); + if (ignoreMissing) { + ctx.status(HttpCode.MULTI_STATUS); + + } else { + ctx.status(HttpServletResponse.SC_BAD_REQUEST); + detailsMap.put("message", + "One or more time series were not found and could not be assigned to the group"); + } + ctx.json(detailsMap); + } + } } @@ -298,6 +322,8 @@ public void create(@NotNull Context ctx) { @OpenApiParam(name = REPLACE_ASSIGNED_TS, type = Boolean.class, description = "Specifies whether to " + "unassign all existing time series before assigning new time series specified in the content body " + "Default: false"), + @OpenApiParam(name = IGNORE_MISSING, type = Boolean.class, description = "If true, do not fail when " + + "time series to assign does not exist. Default is false"), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "office of the user making the request. This is the office that the timeseries, group, and category " + "belong to. If the group and/or category belong to the CWMS office, " @@ -331,8 +357,22 @@ public void update(@NotNull Context ctx, @NotNull String oldGroupId) { timeSeriesGroupDao.unassignForOffice(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId(), office); } - timeSeriesGroupDao.assignTs(group, office); - ctx.status(HttpServletResponse.SC_OK); + boolean ignoreMissing = ctx.queryParamAsClass(IGNORE_MISSING, Boolean.class).getOrDefault(false); + List missingTimeSeries = timeSeriesGroupDao.assignTs(group, office, ignoreMissing); + if (missingTimeSeries.isEmpty()) { + ctx.status(HttpServletResponse.SC_OK); + } else { + Map detailsMap = new HashMap<>(); + detailsMap.put("missing-timeseries", Formats.format(contentType, missingTimeSeries, CwmsId.class)); + if (ignoreMissing) { + ctx.status(HttpCode.MULTI_STATUS); + } else { + ctx.status(HttpServletResponse.SC_BAD_REQUEST); + detailsMap.put("message", + "One or more time series were not found and could not be assigned to the group"); + } + ctx.json(detailsMap); + } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/errors/NotFoundException.java b/cwms-data-api/src/main/java/cwms/cda/api/errors/NotFoundException.java index ec380bf51b..c95629210e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/errors/NotFoundException.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/errors/NotFoundException.java @@ -1,6 +1,8 @@ package cwms.cda.api.errors; +import java.io.Serializable; import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; import javax.servlet.http.HttpServletResponse; @@ -18,6 +20,10 @@ public NotFoundException(String message, Throwable cause) { LOG_LEVEL, new HashMap<>(), cause); } + public NotFoundException(String message, Map details, Throwable cause) { + super(message, DATABASE_SOURCE, message, HttpServletResponse.SC_NOT_FOUND, LOG_LEVEL, details, cause); + } + public NotFoundException(Throwable cause) { super(NOT_FOUND, DATABASE_SOURCE, NOT_FOUND, HttpServletResponse.SC_NOT_FOUND, LOG_LEVEL, new HashMap<>(), cause); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 742887fe6a..3ac354c1cd 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -39,6 +39,7 @@ import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.HandlerType; +import java.io.Serializable; import java.math.BigDecimal; import java.sql.Connection; import java.sql.ResultSetMetaData; @@ -521,7 +522,21 @@ static NotFoundException buildNotFound(RuntimeException input) { NotFoundException exception; if (input.getMessage().contains("ASSIGN_LOC_GROUPS")) { - exception = new NotFoundException("Location group contains assigned locations that do not exist.", cause); + Map errorDetails = new HashMap<>(); + String localizedMessage = cause.getLocalizedMessage(); + if (localizedMessage != null) { + String[] parts = localizedMessage.split("\n"); + if (parts.length > 1) { + localizedMessage = parts[0]; + if (localizedMessage.startsWith("ORA-")) { + localizedMessage = localizedMessage + .substring(localizedMessage.indexOf("LOCATION_ID_NOT_FOUND:") + 23); + } + } + } + errorDetails.put("missing-locations", localizedMessage); + exception = new NotFoundException("Location group contains assigned locations that do not exist.", + errorDetails, cause); } else { exception = new NotFoundException(cause); 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 ec8352873a..615d1cc890 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 @@ -28,9 +28,11 @@ import static org.jooq.impl.DSL.noCondition; import cwms.cda.data.dto.AssignedLocation; +import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -39,7 +41,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Stream; - import org.geojson.Feature; import org.geojson.FeatureCollection; import org.jetbrains.annotations.NotNull; @@ -60,8 +61,8 @@ import usace.cwms.db.jooq.codegen.tables.AV_LOC; import usace.cwms.db.jooq.codegen.tables.AV_LOC_CAT_GRP; import usace.cwms.db.jooq.codegen.tables.AV_LOC_GRP_ASSGN; -import usace.cwms.db.jooq.codegen.udt.records.LOC_ALIAS_ARRAY3; -import usace.cwms.db.jooq.codegen.udt.records.LOC_ALIAS_TYPE3; +import usace.cwms.db.jooq.codegen_latest.udt.records.LOC_ALIAS_ARRAY3; +import usace.cwms.db.jooq.codegen_latest.udt.records.LOC_ALIAS_TYPE3; public final class LocationGroupDao extends JooqDao { @@ -476,19 +477,30 @@ public void delete(String categoryId, String groupId, boolean cascadeDelete, Str /** * Create a location group. * @param group The location group to create. + * @return A list of assigned locations that did not exist when attempting to assign them. */ - public void create(LocationGroup group) { + public List create(LocationGroup group) { + return create(group, false); + } + + /** + * Create a location group. + * @param group The location group to create. + * @param allowPartialAssignment Whether to allow partial assignment. + * @return A list of assigned locations that did not exist when attempting to assign them. + */ + public List create(LocationGroup group, boolean allowPartialAssignment) { String office = group.getOfficeId(); String categoryId = group.getLocationCategory().getId(); - connection(dsl, conn -> { + return connectionResult(dsl, conn -> { DSLContext dslContext = getDslContext(conn, office); - dslContext.transaction((Configuration trx) -> { + return dslContext.transactionResult((Configuration trx) -> { Configuration config = trx.dsl().configuration(); CWMS_LOC_PACKAGE.call_CREATE_LOC_GROUP2(config, categoryId, group.getId(), group.getDescription(), group.getOfficeId(), group.getSharedLocAliasId(), group.getSharedRefLocationId()); - assignLocs(config, group, office); + return assignLocs(config, group, office, allowPartialAssignment); }); }); } @@ -524,6 +536,13 @@ public void unassignAllLocs(LocationGroup group, String office) { }); } + public List assignLocs(LocationGroup group, String office, boolean ignoreMissing) { + return connectionResult(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + return assignLocs(dslContext.configuration(), group, office, ignoreMissing); + }); + } + public void assignLocs(LocationGroup group, String office) { connection(dsl, conn -> { DSLContext dslContext = getDslContext(conn, office); @@ -536,8 +555,24 @@ public void assignLocs(LocationGroup group, String office) { * @param config a DSL configuration to use for the operation * @param group the location group to assign locations to * @param office the office to use for the operation + * @return a list of assigned locations that did not exist when attempting to assign them. + * Empty list if all locations were assigned. */ - public void assignLocs(Configuration config, LocationGroup group, String office) { + public List assignLocs(Configuration config, LocationGroup group, String office) { + return assignLocs(config, group, office, false); + } + + /** + * Used when an appropriate context already exists to avoid opening a second connection. + * @param config a DSL configuration to use for the operation + * @param group the location group to assign locations to + * @param office the office to use for the operation + * @param ignoreMissing whether to fail when attempting to assign a location that does not exist. + * @return a list of assigned locations that did not exist when attempting to assign them. + * Empty list if all locations were assigned. + */ + public List assignLocs(Configuration config, LocationGroup group, String office, boolean ignoreMissing) { + List nonExistentLocs = new ArrayList<>(); List assignedLocations = group.getAssignedLocations(); if (assignedLocations != null) { List collect = assignedLocations.stream() @@ -553,7 +588,15 @@ public void assignLocs(Configuration config, LocationGroup group, String office) .collect(toList()); LOC_ALIAS_ARRAY3 assignedLocs = new LOC_ALIAS_ARRAY3(collect); LocationCategory cat = group.getLocationCategory(); - CWMS_LOC_PACKAGE.call_ASSIGN_LOC_GROUPS3(config, cat.getId(), group.getId(), assignedLocs, office); + LOC_ALIAS_ARRAY3 missingLocs = usace.cwms.db.jooq.codegen_latest.packages.CWMS_LOC_PACKAGE + .call_ASSIGN_LOC_GROUPS_SUPPORTS_MISSING(config, cat.getId(), group.getId(), + assignedLocs, office, formatBool(ignoreMissing)); + if (!missingLocs.isEmpty()) { + for (LOC_ALIAS_TYPE3 loc : missingLocs) { + nonExistentLocs.add(CwmsId.buildCwmsId(office, loc.getLOCATION_ID())); + } + } } + return nonExistentLocs; } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java index e582b09c08..453c6078a1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java @@ -30,6 +30,7 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.data.dao.timeseriesgroup.DELETE_TS_GROUP_CASCADE; import cwms.cda.data.dto.AssignedTimeSeries; +import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.TimeSeriesCategory; import cwms.cda.data.dto.TimeSeriesGroup; import java.math.BigDecimal; @@ -53,8 +54,8 @@ import usace.cwms.db.jooq.codegen.packages.CWMS_TS_PACKAGE; import usace.cwms.db.jooq.codegen.tables.AV_TS_CAT_GRP; import usace.cwms.db.jooq.codegen.tables.AV_TS_GRP_ASSGN; -import usace.cwms.db.jooq.codegen.udt.records.TS_ALIAS_T; -import usace.cwms.db.jooq.codegen.udt.records.TS_ALIAS_TAB_T; +import usace.cwms.db.jooq.codegen_latest.udt.records.TS_ALIAS_T; +import usace.cwms.db.jooq.codegen_latest.udt.records.TS_ALIAS_TAB_T; public class TimeSeriesGroupDao extends JooqDao { @@ -450,21 +451,34 @@ public static void call_DELETE_TS_GROUP_CASCADE(Configuration configuration, Str p.execute(configuration); } + public List create(TimeSeriesGroup group, boolean failIfExists, boolean ignoreNulls) { + return create(group, failIfExists, ignoreNulls, false); + } - public void create(TimeSeriesGroup group, boolean failIfExists, boolean ignoreNulls) { - connection(dsl, c -> { + public List create(TimeSeriesGroup group, boolean failIfExists, boolean ignoreNulls, boolean ignoreMissing) { + return connectionResult(dsl, c -> { Configuration configuration = getDslContext(c, group.getOfficeId()).configuration(); String categoryId = group.getTimeSeriesCategory().getId(); - CWMS_TS_PACKAGE.call_STORE_TS_GROUP(configuration, categoryId, - group.getId(), group.getDescription(), formatBool(failIfExists), + DSLContext dslContext = getDslContext(c, group.getOfficeId()); + return dslContext.transactionResult((Configuration trx) -> { + CWMS_TS_PACKAGE.call_STORE_TS_GROUP(configuration, categoryId, + group.getId(), group.getDescription(), formatBool(failIfExists), formatBool(ignoreNulls), group.getSharedAliasId(), - group.getSharedRefTsId(), group.getOfficeId()); - assignTs(configuration, group, group.getOfficeId(), ignoreNulls); + group.getSharedRefTsId(), group.getOfficeId()); + return assignTs(configuration, group, group.getOfficeId(), ignoreNulls, ignoreMissing); + }); }); } - private void assignTs(Configuration configuration, TimeSeriesGroup group, String office, boolean ignoreNulls) { + private List assignTs(Configuration configuration, TimeSeriesGroup group, + String office, boolean ignoreNulls) { + return assignTs(configuration, group, office, ignoreNulls, false); + } + + private List assignTs(Configuration configuration, TimeSeriesGroup group, + String office, boolean ignoreNulls, boolean ignoreMissing) { List assignedTimeSeries = group.getAssignedTimeSeries(); + List missingTimeSeries = new ArrayList<>(); if (!ignoreNulls && (assignedTimeSeries == null || assignedTimeSeries.isEmpty())) { CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP(configuration, @@ -477,10 +491,20 @@ private void assignTs(Configuration configuration, TimeSeriesGroup group, String .map(TimeSeriesGroupDao::convertToTsAliasType) .collect(toList()); TS_ALIAS_TAB_T assignedLocs = new TS_ALIAS_TAB_T(collect); - CWMS_TS_PACKAGE.call_ASSIGN_TS_GROUPS(configuration, group.getTimeSeriesCategory().getId(), - group.getId(), assignedLocs, office); + TS_ALIAS_TAB_T missingTs = usace.cwms.db.jooq.codegen_latest.packages.CWMS_TS_PACKAGE.call_ASSIGN_TS_GROUPS_SUPPORT_MISSING(configuration, + group.getTimeSeriesCategory().getId(), group.getId(), assignedLocs, office, formatBool(ignoreMissing)); + if (!missingTs.isEmpty()) { + for (TS_ALIAS_T missing : missingTs) { + missingTimeSeries.add(CwmsId.buildCwmsId(office, missing.getTS_ID())); + } + } } } + return missingTimeSeries; + } + + public List assignTs(TimeSeriesGroup group, String office, boolean ignoreMissing) { + return connectionResult(dsl, c -> assignTs(getDslContext(c, office).configuration(),group, office, true, ignoreMissing)); } public void assignTs(TimeSeriesGroup group, String office) { @@ -502,24 +526,21 @@ public void renameTimeSeriesGroup(String oldGroupId, TimeSeriesGroup group) { ); } - public void unassignAllTs(TimeSeriesGroup group, String officeId) { - connection(dsl, c -> // For Default/Default if officeId is 'CWMS' this seems to not unassign - // the assigned timeseries where the office id of the timeseries is SPK. - // Is this a bug? - { - DSLContext dslContext = getDslContext(c, officeId); - - // UNASSIGN_TS_GROUP apparently only unassigns the assignments that are in the - // P_DB_OFFICE_ID ( last parameter) - CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP( - dslContext.configuration(), - group.getTimeSeriesCategory().getId(), group.getId(), - null, "T", group.getOfficeId()); - } + // the assigned timeseries where the office id of the timeseries is SPK. + // Is this a bug? + { + DSLContext dslContext = getDslContext(c, officeId); + + // UNASSIGN_TS_GROUP apparently only unassigns the assignments that are in the + // P_DB_OFFICE_ID ( last parameter) + CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP( + dslContext.configuration(), + group.getTimeSeriesCategory().getId(), group.getId(), + null, "T", group.getOfficeId()); + } ); } - } 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 140f9ddea5..732249c14f 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 @@ -35,6 +35,7 @@ import fixtures.CwmsDataApiSetupCallback; import fixtures.FunctionalSchemas; import fixtures.TestAccounts; +import io.javalin.http.HttpCode; import io.restassured.filter.log.LogDetail; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; @@ -1832,6 +1833,111 @@ void test_post_does_not_write_on_assigned_location_fail(String format) throws Ex .body("id", is(group.getId())); } + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_ignore_missing(String format) throws Exception { + String officeId = user.getOperatingOffice(); + String locationId = "LocMissLocs"; + createLocation(locationId, true, officeId); + String groupId = "LocMissGrp"; + String categoryId = "LocMissCat"; + LocationCategory cat = new LocationCategory(officeId, categoryId, "IntegrTesting"); + InputStream grpStream = this.getClass().getResourceAsStream("/cwms/cda/api/location_group_missing_assignments.json"); + LocationGroup group = Formats.parseContent(new ContentType(Formats.JSON), grpStream, LocationGroup.class); + 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()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/category/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Create Group without ignoring missing locations + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + .body("message", containsString("Location group contains assigned locations that do not exist.")) + .body("details.missing-locations", is("The Location: \"notReal-loc\" does not exist.")); + + //Retrieve Group, assert not found + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + groupId) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + //Create Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(IGNORE_MISSING, true) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpCode.MULTI_STATUS.getStatus())) + .body("missing-locations", equalTo("notReal-loc")); + + //Retrieve Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("id", is(group.getId())) + .body("assigned-locations.size()", is(0)); + } + @Test void testRetrievalTiming() { String officeId = user.getOperatingOffice(); diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/location_group_missing_assignments.json b/cwms-data-api/src/test/resources/cwms/cda/api/location_group_missing_assignments.json new file mode 100644 index 0000000000..cc13f09dac --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/location_group_missing_assignments.json @@ -0,0 +1,19 @@ +{ + "office-id": "SPK", + "id": "LocMissGrp", + "location-category": { + "office-id": "SPK", + "id": "LocMissCat", + "description": "IntegrTesting" + }, + "description": "test group description", + "loc-group-attribute": 0, + "assigned-locations": [ + { + "location-id": "notReal-loc", + "office-id": "SPK", + "ref-location-id": "notReal", + "attribute": 0 + } + ] +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ab4ca0276..28a8725ec9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ jaxb-impl = { module = "com.sun.xml.bind:jaxb-impl", version.ref = "jaxb-impl" } jaxb-core = { module = "com.sun.xml.bind:jaxb-core", version.ref = "jaxb-impl" } cwms-db-jooq-codegen = { module = "mil.army.usace.hec:cwms-db-jooq-codegen_java11", version.ref = "jooq-codegen" } cwms-db-jooq-codegen-legacy = { module = "mil.army.usace.hec:cwms-db-jooq-codegen_java8", version = "24.12.04-2025.01.21" } -cwms-db-jooq-codegen-latest = { module = "mil.army.usace.hec:cwms-db-jooq-codegen_java11", version = "latest-dev_sha256_f1-oracle19c" } +cwms-db-jooq-codegen-latest = { module = "mil.army.usace.hec:cwms-db-jooq-codegen_java11", version = "latest-dev_sha256_e7740339fdf4-oracle19c" } jooq = { module = "org.jooq.pro-java-11:jooq", version.ref ="jooq" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" } From c0754d072f3877eeb0860bbab3c5e22f23b2e9e4 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Thu, 16 Jul 2026 16:56:20 -0700 Subject: [PATCH 2/5] Updated error handling for time series group partial storage. Added integration test. --- .../cda/api/TimeSeriesGroupController.java | 18 +- .../main/java/cwms/cda/data/dao/JooqDao.java | 16 ++ .../api/TimeSeriesGroupControllerTestIT.java | 178 ++++++++++++++++++ 3 files changed, 208 insertions(+), 4 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java index 6355032a79..93df958cc6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java @@ -58,7 +58,6 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.TimeSeriesGroupDao; -import cwms.cda.data.dto.AssignedTimeSeries; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.TimeSeriesGroup; import cwms.cda.formatters.ContentType; @@ -290,8 +289,13 @@ public void create(@NotNull Context ctx) { ctx.status(HttpServletResponse.SC_CREATED); } else { Map detailsMap = new HashMap<>(); - detailsMap.put("missing-timeseries", Formats.format(contentType, missingTimeSeries, - AssignedTimeSeries.class)); + StringBuilder sb = new StringBuilder(); + for (CwmsId cwmsId : missingTimeSeries) { + sb.append(cwmsId.getName()); + sb.append(", "); + } + sb.delete(sb.length() - 2, sb.length()); + detailsMap.put("missing-time-series", sb.toString()); if (ignoreMissing) { ctx.status(HttpCode.MULTI_STATUS); @@ -363,7 +367,13 @@ public void update(@NotNull Context ctx, @NotNull String oldGroupId) { ctx.status(HttpServletResponse.SC_OK); } else { Map detailsMap = new HashMap<>(); - detailsMap.put("missing-timeseries", Formats.format(contentType, missingTimeSeries, CwmsId.class)); + StringBuilder sb = new StringBuilder(); + for (CwmsId cwmsId : missingTimeSeries) { + sb.append(cwmsId.getName()); + sb.append(", "); + } + sb.delete(sb.length() - 2, sb.length()); + detailsMap.put("missing-timeseries", sb.toString()); if (ignoreMissing) { ctx.status(HttpCode.MULTI_STATUS); } else { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 3ac354c1cd..2effeb5d88 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -537,6 +537,22 @@ static NotFoundException buildNotFound(RuntimeException input) { errorDetails.put("missing-locations", localizedMessage); exception = new NotFoundException("Location group contains assigned locations that do not exist.", errorDetails, cause); + } else if (input.getMessage().contains("ASSIGN_TS_GROUPS")) { + Map errorDetails = new HashMap<>(); + String localizedMessage = cause.getLocalizedMessage(); + if (localizedMessage != null) { + String[] parts = localizedMessage.split("\n"); + if (parts.length > 1) { + localizedMessage = parts[0]; + if (localizedMessage.startsWith("ORA-")) { + localizedMessage = localizedMessage + .substring(localizedMessage.indexOf("TS_ID_NOT_FOUND:") + 17); + } + } + } + errorDetails.put("missing-time-series", localizedMessage); + exception = new NotFoundException("Time series group contains assigned time series that do not exist.", + errorDetails, cause); } else { exception = new NotFoundException(cause); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java index 9ffd611348..bfb0800053 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java @@ -32,6 +32,7 @@ import static cwms.cda.api.Controllers.END; import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; import static cwms.cda.api.Controllers.GROUP_OFFICE_ID; +import static cwms.cda.api.Controllers.IGNORE_MISSING; import static cwms.cda.api.Controllers.IGNORE_NULLS; import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.REPLACE_ASSIGNED_LOCS; @@ -69,6 +70,7 @@ import fixtures.CwmsDataApiSetupCallback; import fixtures.FunctionalSchemas; import fixtures.TestAccounts; +import io.javalin.http.HttpCode; import io.restassured.filter.log.LogDetail; import io.restassured.path.json.JsonPath; import io.restassured.response.Response; @@ -408,6 +410,182 @@ void test_create_read_delete(String format) throws Exception { .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_create_ignore_missing(String format) throws Exception { + String officeId = user.getOperatingOffice(); + String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; + String nonExistentTimeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.notReal"; + createLocation(timeSeriesId.split("\\.")[0],true,officeId); + TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_create_read_delete", "IntegrationTesting"); + TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_create_read_delete", "IntegrationTesting", + "sharedTsAliasId", timeSeriesId); + List assignedTimeSeries = group.getAssignedTimeSeries(); + + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); + + assignedTimeSeries.add(new AssignedTimeSeries(officeId,timeSeriesId, "AliasId", timeSeriesId, 1)); + assignedTimeSeries.add(new AssignedTimeSeries(officeId,nonExistentTimeSeriesId, "AliasId", nonExistentTimeSeriesId, 1)); + ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.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()) + .queryParam(OFFICE, officeId) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/category") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + //Create Group, not ignoring missing time series + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + .body("message", equalTo("Time series group contains assigned time series that do not exist.")) + .body("details.missing-time-series", equalTo("The timeseries identifier \"" + + nonExistentTimeSeriesId + "\" was not found for office \"" + officeId + "\"")); + //Read Empty + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + //Create Group, ignore missing time series + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(IGNORE_MISSING, true) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpCode.MULTI_STATUS.getStatus())) + .body("missing-time-series", equalTo(nonExistentTimeSeriesId)); + //Read Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(group.getOfficeId())); + //Clear Assigned TS + group.getAssignedTimeSeries().clear(); + groupXml = Formats.format(contentType, group); + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) + .queryParam(REPLACE_ASSIGNED_TS, true) + .queryParam(OFFICE, group.getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/timeseries/group/"+ group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + //Delete Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/group/" + group.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + //Read Empty + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + //Delete Category + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/category/" + group.getTimeSeriesCategory().getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + @ParameterizedTest @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) void test_create_read_delete_LRTS(String format) throws Exception { From 6d2d62e3513ebc973932ece66e2d7b0a2cf29590 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Fri, 17 Jul 2026 10:15:12 -0700 Subject: [PATCH 3/5] Resolve failing lock test --- .../data/dto/locationlevel/LocationLevel.java | 9 ++++++++- .../test/java/cwms/cda/api/LockControllerIT.java | 16 ++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java index 8ab3d09df0..76262673e5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevel.java @@ -29,6 +29,8 @@ import cwms.cda.data.dto.catalog.LocationAlias; import java.math.BigDecimal; import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -95,7 +97,8 @@ public abstract class LocationLevel extends CwmsDTO { @Schema(description = "Units the provided levels are in") private final String levelUnitsId; - @Schema(description = "The date/time at which this location level configuration takes effect.") + @Schema(description = "The date/time at which this location level configuration takes effect. " + + "Must be limited to minute accuracy.") @JsonFormat(shape = JsonFormat.Shape.STRING) private final Instant levelDate; @@ -501,4 +504,8 @@ protected void validateInternal(CwmsDTOValidator validator) { validator.required(getLocationLevelId(), "location-level-id"); validator.required(getLevelDate(), "level-date"); } + + public static ZonedDateTime truncateDate(ZonedDateTime date) { + return date.truncatedTo(ChronoUnit.MINUTES); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java index cf0204d03f..1303f1235e 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java @@ -35,6 +35,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.enums.Nation; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.DeleteRule; @@ -45,10 +46,10 @@ import cwms.cda.data.dao.location.kind.LockDao; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.Location; -import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; -import cwms.cda.data.dto.locationlevel.LocationLevel; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.location.kind.LockLocationLevelRef; +import cwms.cda.data.dto.locationlevel.ConstantLocationLevel; +import cwms.cda.data.dto.locationlevel.LocationLevel; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import fixtures.CwmsDataApiSetupCallback; @@ -65,7 +66,6 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; -import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; import org.apache.commons.io.IOUtils; @@ -1049,28 +1049,28 @@ private static PROJECT_OBJ_T buildProject(Location projectLocation) { private List createLocationLevelList(Lock lock) { List retVal = new ArrayList<>(); - var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLowWaterLowerPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getLowWaterLowerPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getLowWaterLowerPoolLocationLevel().getLevelValue()) .build(); retVal.add(lowLowerLevel); - var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLowWaterUpperPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getLowWaterUpperPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getLowWaterUpperPoolLocationLevel().getLevelValue()) .build(); retVal.add(lowUpperLevel); - var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getHighWaterLowerPoolLocationLevel().getOfficeId()) .withSpecifiedLevelId(lock.getHighWaterLowerPoolLocationLevel().getSpecifiedLevelId()) .withConstantValue(lock.getHighWaterLowerPoolLocationLevel().getLevelValue()) .build(); retVal.add(highLowerLevel); - var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant()) + var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getHighWaterUpperPoolLocationLevel().getOfficeId()) @@ -1078,7 +1078,7 @@ private List createLocationLevelList(Lock lock) { .withConstantValue(lock.getHighWaterUpperPoolLocationLevel().getLevelValue()) .build(); retVal.add(highUpperLevel); - var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), ZonedDateTime.now().toInstant()) + var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant()) .withLevelUnitsId(lock.getElevationUnits()) .withOfficeId(lock.getLocation().getOfficeId()) .withSpecifiedLevelId("Warning Buffer") From 29ef2745a8d88f9ce1394ad289dacc3a4dc46952 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 20 Jul 2026 11:18:25 -0700 Subject: [PATCH 4/5] Fixed failing location test --- .../src/main/java/cwms/cda/api/LocationController.java | 2 +- .../src/test/java/cwms/cda/api/LocationControllerTestIT.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java index 3acf92fc97..18b890decb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java @@ -439,7 +439,7 @@ public void delete(@NotNull Context ctx, @NotNull String locationId) { ctx.status(HttpServletResponse.SC_OK).json(re); } catch (DataAccessException ex) { SQLException cause = ex.getCause(SQLException.class); - if (cause != null && cause.getErrorCode() == 20031) { + if (cause != null && cause.getErrorCode() == 20056) { throw new DeleteConflictException("Unable to delete requested location: " + locationId + " for office: " + office, cause); } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java index c4ccf7173d..b0bc5564c1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java @@ -537,6 +537,7 @@ void test_location_create_get_bad_units_delete() throws Exception { .accept(Formats.JSON) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, true) .when() .redirects().follow(true) .redirects().max(3) @@ -1228,8 +1229,8 @@ void testDeleteConflict() throws Exception { .body("message", equalTo("Cannot delete this record because it is linked to other data in CWMS")) .body("details.message", equalTo("Unable to delete requested location: " - + "Putah_Creek for office: SPK: ORA-20031: CAN_NOT_DELETE_LOC_1: " - + "Can not delete location: \"Putah_Creek\" because Timeseries Identifiers exist.")); + + "Putah_Creek for office: SPK: ORA-20056: CAN_NOT_DELETE_LOC_2: " + + "Can not delete location: \"Putah_Creek\" because dependent data exists: time series identifiers=1; location geometry=1.")); } @Test From b2ae46002173ddb3ae6fa980b4079e65404bbd11 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 20 Jul 2026 12:06:19 -0700 Subject: [PATCH 5/5] Revert "Fixed failing location test" This reverts commit 29ef2745a8d88f9ce1394ad289dacc3a4dc46952. --- .../src/main/java/cwms/cda/api/LocationController.java | 2 +- .../src/test/java/cwms/cda/api/LocationControllerTestIT.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java index 18b890decb..3acf92fc97 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java @@ -439,7 +439,7 @@ public void delete(@NotNull Context ctx, @NotNull String locationId) { ctx.status(HttpServletResponse.SC_OK).json(re); } catch (DataAccessException ex) { SQLException cause = ex.getCause(SQLException.class); - if (cause != null && cause.getErrorCode() == 20056) { + if (cause != null && cause.getErrorCode() == 20031) { throw new DeleteConflictException("Unable to delete requested location: " + locationId + " for office: " + office, cause); } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java index b0bc5564c1..c4ccf7173d 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java @@ -537,7 +537,6 @@ void test_location_create_get_bad_units_delete() throws Exception { .accept(Formats.JSON) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, officeId) - .queryParam(CASCADE_DELETE, true) .when() .redirects().follow(true) .redirects().max(3) @@ -1229,8 +1228,8 @@ void testDeleteConflict() throws Exception { .body("message", equalTo("Cannot delete this record because it is linked to other data in CWMS")) .body("details.message", equalTo("Unable to delete requested location: " - + "Putah_Creek for office: SPK: ORA-20056: CAN_NOT_DELETE_LOC_2: " - + "Can not delete location: \"Putah_Creek\" because dependent data exists: time series identifiers=1; location geometry=1.")); + + "Putah_Creek for office: SPK: ORA-20031: CAN_NOT_DELETE_LOC_1: " + + "Can not delete location: \"Putah_Creek\" because Timeseries Identifiers exist.")); } @Test