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..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
@@ -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,7 @@
import com.google.common.flogger.FluentLogger;
import cwms.cda.api.errors.CdaError;
import cwms.cda.data.dao.TimeSeriesGroupDao;
+import cwms.cda.data.dto.CwmsId;
import cwms.cda.data.dto.TimeSeriesGroup;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
@@ -71,7 +73,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 +247,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 +282,31 @@ 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<>();
+ 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);
+
+ } 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 +326,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 +361,28 @@ 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<>();
+ 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 {
+ 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..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
@@ -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,37 @@ 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 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/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/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/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/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")
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 {
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 523452fae6..6a68403a72 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -57,7 +57,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" }