From 54df11bad70a5dc607704380aec0e6ffcc1b6e5d Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 20 Jul 2026 15:59:40 -0700 Subject: [PATCH 1/4] Initial implementation of bulk vertical datum info retrieval endpoint --- .../src/main/java/cwms/cda/ApiServlet.java | 3 +- .../main/java/cwms/cda/api/Controllers.java | 4 +- .../cwms/cda/api/VerticalDatumController.java | 98 +++++++++--- .../cwms/cda/data/dao/VerticalDatumDao.java | 45 +++++- .../api/VerticalDatumControllerTestIT.java | 148 ++++++++++++++++++ 5 files changed, 273 insertions(+), 25 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index 57a679c47e..827a000990 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -91,9 +91,9 @@ import cwms.cda.api.TimeSeriesController; import cwms.cda.api.TimeSeriesFilteredController; import cwms.cda.api.TimeSeriesGroupController; -import cwms.cda.api.TimeSeriesVersionsController; import cwms.cda.api.TimeSeriesIdentifierDescriptorController; import cwms.cda.api.TimeSeriesRecentController; +import cwms.cda.api.TimeSeriesVersionsController; import cwms.cda.api.TimeZoneController; import cwms.cda.api.TurbineChangesDeleteController; import cwms.cda.api.TurbineChangesGetController; @@ -473,6 +473,7 @@ protected void configureRoutes() { VerticalDatumController vdiController = new VerticalDatumController(metrics); String vdiPath = format("/location/{%s}/vertical-datum", Controllers.LOCATION_ID); + get("/location/vertical-datum", vdiController::getAll); get(vdiPath, ctx -> vdiController.getOne(ctx, ctx.pathParam(Controllers.LOCATION_ID))); addCacheControl(vdiPath, 5, TimeUnit.MINUTES); post(vdiPath, vdiController::create, requiredRoles); 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..511b98615c 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; @@ -75,6 +73,8 @@ public final class Controllers { public static final String LIKE = "like"; + public static final String OVERWRITE = "overwrite"; + public static final String UNIT_SYSTEM = "unit-system"; public static final String TIMESERIES_CATEGORY_LIKE = "timeseries-category-like"; diff --git a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java index 2bdc82aa27..53f19018f8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java @@ -29,7 +29,9 @@ import static cwms.cda.api.Controllers.DELETE; import static cwms.cda.api.Controllers.GET_ONE; import static cwms.cda.api.Controllers.LOCATION_ID; +import static cwms.cda.api.Controllers.LOCATION_MASK; import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.OVERWRITE; import static cwms.cda.api.Controllers.RESULTS; import static cwms.cda.api.Controllers.SIZE; import static cwms.cda.api.Controllers.UNIT; @@ -42,6 +44,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.google.common.flogger.FluentLogger; +import cwms.cda.api.errors.AlreadyExists; import cwms.cda.api.errors.CdaError; import cwms.cda.api.errors.ExceptionTraceSupport; import cwms.cda.data.dao.VerticalDatumDao; @@ -59,6 +62,7 @@ import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.io.IOException; +import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -68,6 +72,7 @@ public final class VerticalDatumController implements CrudHandler { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); // NOTE: manually expanded due to limits of OpenApi Annotations. private static final String VDI_PATH = "/location/{location-id}/vertical-datum"; + private static final String VDI_ALL_PATH = "/location/vertical-datum"; private final MetricRegistry metrics; private final Histogram requestResultSize; @@ -81,9 +86,49 @@ private Timer.Context markAndTime(String subject) { return Controllers.markAndTime(metrics, getClass().getName(), subject); } + @OpenApi( + queryParams = { + @OpenApiParam(name = LOCATION_MASK, description = "Filters on the location ID."), + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office."), + @OpenApiParam(name = UNIT, + description = "Specifies the unit of measure for elevation/offsets (e.g., m or ft). Default is m.") + }, + responses = { + @OpenApiResponse(status = Controllers.STATUS_200, + content = {@OpenApiContent(type = Formats.JSONV1, from = VerticalDatumInfo.class), + @OpenApiContent(type = Formats.JSON, from = VerticalDatumInfo.class), + @OpenApiContent(type = Formats.XMLV1, from = VerticalDatumInfo.class), + @OpenApiContent(type = Formats.XML, from = VerticalDatumInfo.class)}) + }, + description = "Returns Vertical Datum Info for all locations.", + path = VDI_ALL_PATH, + tags = {LOCATIONS_TAG} + ) @Override public void getAll(@NotNull Context ctx) { - ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); + String office = requiredParam(ctx, OFFICE); + String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault("m"); + String locationMask = ctx.queryParamAsClass(LOCATION_MASK, String.class).getOrDefault(null); + try (Timer.Context ignored = markAndTime(GET_ONE)) { + DSLContext dsl = getDslContext(ctx); + VerticalDatumDao dao = new VerticalDatumDao(dsl); + List info = dao.retrieveVerticalDatumInfoList(office, locationMask, units); + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, info, VerticalDatumInfo.class); + requestResultSize.update(serialized.length()); + ctx.status(HttpServletResponse.SC_OK); + + byte[] bytes = serialized.getBytes(); + ctx.header(Header.CONTENT_LENGTH, String.valueOf(bytes.length)); + ctx.res.getOutputStream().write(bytes); + } catch (IOException ex) { + CdaError error = ExceptionTraceSupport.buildError(ctx, + "Failed to process request to retrieve Vertical Datum Info", ex); + logger.atSevere().withCause(ex).log("Failed to process request to retrieve Vertical Datum Info"); + ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(error); + } } @OpenApi( @@ -117,7 +162,6 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) { ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); ctx.contentType(contentType.toString()); String serialized = Formats.format(contentType, info); - ctx.status(HttpServletResponse.SC_OK); requestResultSize.update(serialized.length()); ctx.status(HttpServletResponse.SC_OK); @@ -133,42 +177,54 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) { } @OpenApi( - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.JSONV1), - @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.XMLV1) - }, - required = true), - queryParams = { - @OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the location id for this vertical-datum-info."), - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office.") + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.JSONV1), + @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.XMLV1) }, - description = "Create Vertical Datum Info for a Location", - method = HttpMethod.POST, - path = VDI_PATH, - tags = {LOCATIONS_TAG}, - responses = { - @OpenApiResponse(status = Controllers.STATUS_201, description = "Vertical Datum Info successfully stored to CWMS.") - } + required = true), + queryParams = { + @OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the location id for this vertical-datum-info."), + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office."), + @OpenApiParam(name = OVERWRITE, type = Boolean.class, description = "If true, will overwrite any existing " + + "vertical-datum-info for the specified location. Default is false.") + }, + description = "Create Vertical Datum Info for a Location", + method = HttpMethod.POST, + path = VDI_PATH, + tags = {LOCATIONS_TAG}, + responses = { + @OpenApiResponse(status = Controllers.STATUS_201, + description = "Vertical Datum Info successfully stored to CWMS.") + } ) @Override public void create(@NotNull Context ctx) { try (Timer.Context ignored = markAndTime(CREATE)) { String formatHeader = ctx.req.getContentType(); + boolean overwrite = ctx.queryParamAsClass(OVERWRITE, Boolean.class).getOrDefault(false); ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); VerticalDatumInfo info = Formats.parseContent(contentType, ctx.body(), VerticalDatumInfo.class); //allow locationId and office to be specified in either the body or as query params, but require them to be present in one of those places String locationId = info.getLocation(); String office = info.getOffice(); - if(locationId == null || locationId.isBlank()) { + if (locationId == null || locationId.isBlank()) { locationId = requiredParam(ctx, LOCATION_ID); } - if(office == null || office.isBlank()) { + if (office == null || office.isBlank()) { office = requiredParam(ctx, OFFICE); } DSLContext dsl = getDslContext(ctx); VerticalDatumDao dao = new VerticalDatumDao(dsl); - dao.createVerticalDatumInfo(office, locationId, info); + try { + dao.createVerticalDatumInfo(office, locationId, info); + } catch (AlreadyExists ex) { + if (overwrite) { + dao.updateVerticalDatumInfo(office, locationId, info); + } else { + throw ex; + } + } StatusResponse re = new StatusResponse(office, "Vertical Datum Info successfully stored to CWMS.", locationId); ctx.status(HttpServletResponse.SC_CREATED).json(re); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java index 1a072dbce1..6389f3c3b8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java @@ -27,9 +27,14 @@ import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.VerticalDatumInfo; import cwms.cda.formatters.xml.XMLv1; +import java.util.ArrayList; +import java.util.List; +import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.Record1; +import org.jooq.Result; import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; +import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; import usace.cwms.db.jooq.codegen.tables.AV_VERT_DATUM_OFFSET; /** @@ -51,6 +56,44 @@ public VerticalDatumInfo retrieveVerticalDatumInfo(String officeId, String locat }); } + public List retrieveVerticalDatumInfoList(String officeId, String locMask, String units) { + AV_VERT_DATUM_OFFSET view = AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET; + List resultList = new ArrayList<>(); + return connectionResult(dsl, conn -> { + DSLContext ctx = getDslContext(conn, officeId); + Condition whereCondition = view.OFFICE_ID.eq(officeId); + + if (locMask != null && !locMask.isEmpty()) { + whereCondition = whereCondition.and(view.LOCATION_ID.like(locMask)); + } + Result> result = + ctx.select(view).from(view) + .where(whereCondition) + .fetch(); + for (Record1 rec : result) { + usace.cwms.db.jooq.codegen.tables.records.AV_VERT_DATUM_OFFSET tab = rec.value1(); + String desc = tab.getDESCRIPTION(); + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(tab.getOFFICE_ID()) + .withLocation(tab.getLOCATION_ID()) + .withNativeDatum(tab.getVERTICAL_DATUM_ID_1()) + .withLocalDatumName(tab.getVERTICAL_DATUM_ID_2()) + .withOffset(desc.contains("ESTIMATE"), tab.getVERTICAL_DATUM_ID_2(), tab.getOFFSET()) + .build(); + if (!units.equalsIgnoreCase("m")) { + vdi = new VerticalDatumInfo.Builder() + .from(vdi) + .withUnit(units) + .withElevation(CWMS_UTIL_PACKAGE.call_CONVERT_UNITS(ctx.configuration(), vdi.getElevation(), + "m", units)) + .build(); + } + resultList.add(vdi); + } + return resultList; + }); + } + public void createVerticalDatumInfo(String officeId, String locationId, VerticalDatumInfo vdi) { connection(dsl, conn -> { DSLContext ctx = getDslContext(conn, officeId); @@ -98,7 +141,7 @@ private void verifyVerticalDatumInfoExists(DSLContext ctx, String officeId, Stri .where(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.LOCATION_ID.eq(locationId)) .and(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.OFFICE_ID.eq(officeId)) .fetchOne(); - if(result == null) { + if (result == null) { throw new NotFoundException("No vertical datum info found for location " + locationId + " in office " + officeId); } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java index 3b211002a1..46f7786adc 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java @@ -35,6 +35,7 @@ import fixtures.TestAccounts; import fixtures.TestAccounts.KeyUser; import io.restassured.filter.log.LogDetail; +import java.util.List; import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterAll; @@ -247,6 +248,97 @@ void test_vertical_datum_crud(ContentType contentType) { .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + @MethodSource("provideFormats") + @ParameterizedTest + void test_vertical_datum_getAll(ContentType contentType) { + // Build a VerticalDatumInfo payload + VerticalDatumInfo.Offset[] offsets = new VerticalDatumInfo.Offset[] { + new VerticalDatumInfo.Offset(true, "NAVD-88", -0.5) + }; + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(OFFICE_ID) + .withLocation(TEST_LOCATION) + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(100.0) + .withOffsets(offsets) + .build(); + + String vdiPayload = Formats.format(contentType, vdi); + + KeyUser user = KeyUser.SPK_NORMAL; + + // CREATE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // GET + String getBody = + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + .asString(); + + List got = Formats.parseContentList(contentType, getBody, VerticalDatumInfo.class); + assertEquals(1, got.size()); + assertEquals(100.0, got.get(0).getElevation(), 0.001); + assertEquals("NGVD-29", got.get(0).getNativeDatum()); + + // DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + //VERIFY DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + @MethodSource("provideFormats") @ParameterizedTest void test_create_vertical_datum_already_exists_fails(ContentType contentType) { @@ -302,6 +394,62 @@ void test_create_vertical_datum_already_exists_fails(ContentType contentType) { .statusCode(is(HttpServletResponse.SC_CONFLICT)); } + @MethodSource("provideFormats") + @ParameterizedTest + void test_create_vertical_datum_already_exists_overwrite(ContentType contentType) { + // Build a VerticalDatumInfo payload + VerticalDatumInfo.Offset[] offsets = new VerticalDatumInfo.Offset[] { + new VerticalDatumInfo.Offset(true, "NAVD-88", -0.5) + }; + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(OFFICE_ID) + .withLocation(TEST_LOCATION) + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(100.0) + .withOffsets(offsets) + .build(); + + String vdiPayload = Formats.format(contentType, vdi); + + KeyUser user = KeyUser.SPK_NORMAL; + + // First CREATE should succeed + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Second CREATE with same payload should succeed + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.OVERWRITE, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + @MethodSource("provideFormats") @ParameterizedTest void test_update_vertical_datum_not_found_returns_404(ContentType contentType) { From 0474876f0375f4588459411285fb137b4fb9824c Mon Sep 17 00:00:00 2001 From: zack-rma Date: Thu, 13 Aug 2026 15:15:30 -0700 Subject: [PATCH 2/4] Updated to use db procedure. Bumped JOOQ codegen for latest schema --- .../cwms/cda/api/VerticalDatumController.java | 19 +++++---- .../cwms/cda/data/dao/VerticalDatumDao.java | 37 +++------------- .../api/VerticalDatumControllerTestIT.java | 42 ++++++++----------- gradle/libs.versions.toml | 2 +- 4 files changed, 36 insertions(+), 64 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java index 53f19018f8..a8c6a85781 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java @@ -35,6 +35,7 @@ import static cwms.cda.api.Controllers.RESULTS; import static cwms.cda.api.Controllers.SIZE; import static cwms.cda.api.Controllers.UNIT; +import static cwms.cda.api.Controllers.UNIT_SYSTEM; import static cwms.cda.api.Controllers.UPDATE; import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.api.LocationController.LOCATIONS_TAG; @@ -44,6 +45,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.google.common.flogger.FluentLogger; +import cwms.cda.api.enums.UnitSystem; import cwms.cda.api.errors.AlreadyExists; import cwms.cda.api.errors.CdaError; import cwms.cda.api.errors.ExceptionTraceSupport; @@ -73,6 +75,7 @@ public final class VerticalDatumController implements CrudHandler { // NOTE: manually expanded due to limits of OpenApi Annotations. private static final String VDI_PATH = "/location/{location-id}/vertical-datum"; private static final String VDI_ALL_PATH = "/location/vertical-datum"; + private static final String ERROR_MESSAGE = "Failed to process request to retrieve Vertical Datum Info"; private final MetricRegistry metrics; private final Histogram requestResultSize; @@ -90,8 +93,8 @@ private Timer.Context markAndTime(String subject) { queryParams = { @OpenApiParam(name = LOCATION_MASK, description = "Filters on the location ID."), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office."), - @OpenApiParam(name = UNIT, - description = "Specifies the unit of measure for elevation/offsets (e.g., m or ft). Default is m.") + @OpenApiParam(name = UNIT_SYSTEM, + description = "Specifies the unit system of measure for elevation/offsets (SI or EN). Default is EN.") }, responses = { @OpenApiResponse(status = Controllers.STATUS_200, @@ -107,12 +110,12 @@ private Timer.Context markAndTime(String subject) { @Override public void getAll(@NotNull Context ctx) { String office = requiredParam(ctx, OFFICE); - String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault("m"); + String unitSystem = ctx.queryParamAsClass(UNIT_SYSTEM, String.class).getOrDefault(UnitSystem.SI.getValue()); String locationMask = ctx.queryParamAsClass(LOCATION_MASK, String.class).getOrDefault(null); try (Timer.Context ignored = markAndTime(GET_ONE)) { DSLContext dsl = getDslContext(ctx); VerticalDatumDao dao = new VerticalDatumDao(dsl); - List info = dao.retrieveVerticalDatumInfoList(office, locationMask, units); + List info = dao.retrieveVerticalDatumInfoList(office, locationMask, unitSystem); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); ctx.contentType(contentType.toString()); @@ -125,8 +128,8 @@ public void getAll(@NotNull Context ctx) { ctx.res.getOutputStream().write(bytes); } catch (IOException ex) { CdaError error = ExceptionTraceSupport.buildError(ctx, - "Failed to process request to retrieve Vertical Datum Info", ex); - logger.atSevere().withCause(ex).log("Failed to process request to retrieve Vertical Datum Info"); + ERROR_MESSAGE, ex); + logger.atSevere().withCause(ex).log(ERROR_MESSAGE); ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(error); } } @@ -170,8 +173,8 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) { ctx.res.getOutputStream().write(bytes); } catch (IOException ex) { CdaError error = ExceptionTraceSupport.buildError(ctx, - "Failed to process request to retrieve Vertical Datum Info", ex); - logger.atSevere().withCause(ex).log("Failed to process request to retrieve Vertical Datum Info"); + ERROR_MESSAGE, ex); + logger.atSevere().withCause(ex).log(ERROR_MESSAGE); ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(error); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java index 6389f3c3b8..5737a8fac7 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java @@ -29,13 +29,11 @@ import cwms.cda.formatters.xml.XMLv1; import java.util.ArrayList; import java.util.List; -import org.jooq.Condition; import org.jooq.DSLContext; import org.jooq.Record1; -import org.jooq.Result; import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; -import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; import usace.cwms.db.jooq.codegen.tables.AV_VERT_DATUM_OFFSET; +import usace.cwms.db.jooq.codegen_latest.udt.records.CLOB_TAB_T; /** * DAO responsible for CRUD operations on Vertical Datum Info for a Location. @@ -57,37 +55,14 @@ public VerticalDatumInfo retrieveVerticalDatumInfo(String officeId, String locat } public List retrieveVerticalDatumInfoList(String officeId, String locMask, String units) { - AV_VERT_DATUM_OFFSET view = AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET; List resultList = new ArrayList<>(); return connectionResult(dsl, conn -> { DSLContext ctx = getDslContext(conn, officeId); - Condition whereCondition = view.OFFICE_ID.eq(officeId); - - if (locMask != null && !locMask.isEmpty()) { - whereCondition = whereCondition.and(view.LOCATION_ID.like(locMask)); - } - Result> result = - ctx.select(view).from(view) - .where(whereCondition) - .fetch(); - for (Record1 rec : result) { - usace.cwms.db.jooq.codegen.tables.records.AV_VERT_DATUM_OFFSET tab = rec.value1(); - String desc = tab.getDESCRIPTION(); - VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() - .withOffice(tab.getOFFICE_ID()) - .withLocation(tab.getLOCATION_ID()) - .withNativeDatum(tab.getVERTICAL_DATUM_ID_1()) - .withLocalDatumName(tab.getVERTICAL_DATUM_ID_2()) - .withOffset(desc.contains("ESTIMATE"), tab.getVERTICAL_DATUM_ID_2(), tab.getOFFSET()) - .build(); - if (!units.equalsIgnoreCase("m")) { - vdi = new VerticalDatumInfo.Builder() - .from(vdi) - .withUnit(units) - .withElevation(CWMS_UTIL_PACKAGE.call_CONVERT_UNITS(ctx.configuration(), vdi.getElevation(), - "m", units)) - .build(); - } + String mask = locMask == null ? "%" : locMask; + CLOB_TAB_T datumInfo = usace.cwms.db.jooq.codegen_latest.packages.CWMS_LOC_PACKAGE + .call_GET_VERTICAL_DATUM_INFO_LIST(ctx.configuration(), officeId, mask, units); + for (String info : datumInfo) { + VerticalDatumInfo vdi = TimeSeriesDaoImpl.parseVerticalDatumInfo(info); resultList.add(vdi); } return resultList; diff --git a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java index 46f7786adc..a65687052b 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java @@ -35,9 +35,9 @@ import fixtures.TestAccounts; import fixtures.TestAccounts.KeyUser; import io.restassured.filter.log.LogDetail; -import java.util.List; +import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; - +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -45,7 +45,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; @Tag("integration") final class VerticalDatumControllerTestIT extends DataApiTestIT { @@ -286,27 +285,22 @@ void test_vertical_datum_getAll(ContentType contentType) { .statusCode(is(HttpServletResponse.SC_CREATED)); // GET - String getBody = - given() - .log().ifValidationFails(LogDetail.ALL, true) - .accept(contentType.toString()) - .queryParam(OFFICE, OFFICE_ID) - .queryParam(Controllers.UNIT, "m") - .when() - .redirects().follow(true) - .redirects().max(3) - .get("/location/vertical-datum") - .then() - .log().ifValidationFails(LogDetail.ALL, true) - .assertThat() - .statusCode(is(HttpServletResponse.SC_OK)) - .extract() - .asString(); - - List got = Formats.parseContentList(contentType, getBody, VerticalDatumInfo.class); - assertEquals(1, got.size()); - assertEquals(100.0, got.get(0).getElevation(), 0.001); - assertEquals("NGVD-29", got.get(0).getNativeDatum()); + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("size()", Matchers.equalTo(1)) + .body("[0].elevation", Matchers.closeTo(100, 0.001)) + .body("native-datum", Matchers.equalTo("NGVD-29")); // DELETE given() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7b23f5b1f2..ee7f9781f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ jaxb-api = "2.3.1" jaxb-impl = "3.0.2" jooq-codegen = "26.02.17-RC01-oracle19c" -codegen-latest = "latest-dev_sha256_e7740339fdf4-oracle19c" +codegen-latest = "latest-dev_sha256_b91430b177ea-oracle19c" codegen-legacy = "24.12.04-2025.01.21" jooq = "3.18.7-jdk11" slf4j = "2.0.17" From fa7cabaf9ab0354b4c70114499e32e678aa16d8f Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 17 Aug 2026 11:26:04 -0700 Subject: [PATCH 3/4] Updated dao return type. Added new DTO for list of vertical datum. Updated controller and integration test. --- .../cwms/cda/api/VerticalDatumController.java | 16 ++-- .../cwms/cda/data/dao/VerticalDatumDao.java | 13 ++-- .../cwms/cda/data/dto/VerticalDatumInfo.java | 19 +++++ .../cda/data/dto/VerticalDatumInfoList.java | 63 ++++++++++++++++ .../api/VerticalDatumControllerTestIT.java | 75 ++++++++++++++++++- .../cwms/cda/api/vertical-datum-bulk.xml | 21 ++++++ gradle/libs.versions.toml | 2 +- 7 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml diff --git a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java index a8c6a85781..319299f886 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java @@ -44,6 +44,7 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.google.common.flogger.FluentLogger; import cwms.cda.api.enums.UnitSystem; import cwms.cda.api.errors.AlreadyExists; @@ -52,6 +53,7 @@ import cwms.cda.data.dao.VerticalDatumDao; import cwms.cda.data.dto.StatusResponse; import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.data.dto.VerticalDatumInfoList; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import io.javalin.apibuilder.CrudHandler; @@ -98,10 +100,10 @@ private Timer.Context markAndTime(String subject) { }, responses = { @OpenApiResponse(status = Controllers.STATUS_200, - content = {@OpenApiContent(type = Formats.JSONV1, from = VerticalDatumInfo.class), - @OpenApiContent(type = Formats.JSON, from = VerticalDatumInfo.class), - @OpenApiContent(type = Formats.XMLV1, from = VerticalDatumInfo.class), - @OpenApiContent(type = Formats.XML, from = VerticalDatumInfo.class)}) + content = {@OpenApiContent(type = Formats.JSONV1, from = VerticalDatumInfoList.class), + @OpenApiContent(type = Formats.JSON, from = VerticalDatumInfoList.class), + @OpenApiContent(type = Formats.XMLV1, from = VerticalDatumInfoList.class), + @OpenApiContent(type = Formats.XML, from = VerticalDatumInfoList.class)}) }, description = "Returns Vertical Datum Info for all locations.", path = VDI_ALL_PATH, @@ -115,11 +117,11 @@ public void getAll(@NotNull Context ctx) { try (Timer.Context ignored = markAndTime(GET_ONE)) { DSLContext dsl = getDslContext(ctx); VerticalDatumDao dao = new VerticalDatumDao(dsl); - List info = dao.retrieveVerticalDatumInfoList(office, locationMask, unitSystem); + VerticalDatumInfoList vdiList = dao.retrieveVerticalDatumInfoList(office, locationMask, unitSystem); String formatHeader = ctx.header(Header.ACCEPT); - ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); + ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfoList.class); ctx.contentType(contentType.toString()); - String serialized = Formats.format(contentType, info, VerticalDatumInfo.class); + String serialized = Formats.format(contentType, vdiList); requestResultSize.update(serialized.length()); ctx.status(HttpServletResponse.SC_OK); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java index 5737a8fac7..10797d86e7 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java @@ -26,7 +26,9 @@ import cwms.cda.api.errors.AlreadyExists; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.data.dto.VerticalDatumInfoList; import cwms.cda.formatters.xml.XMLv1; +import java.sql.Clob; import java.util.ArrayList; import java.util.List; import org.jooq.DSLContext; @@ -54,19 +56,20 @@ public VerticalDatumInfo retrieveVerticalDatumInfo(String officeId, String locat }); } - public List retrieveVerticalDatumInfoList(String officeId, String locMask, String units) { + public VerticalDatumInfoList retrieveVerticalDatumInfoList(String officeId, String locMask, String units) { List resultList = new ArrayList<>(); - return connectionResult(dsl, conn -> { + connection(dsl, conn -> { DSLContext ctx = getDslContext(conn, officeId); String mask = locMask == null ? "%" : locMask; CLOB_TAB_T datumInfo = usace.cwms.db.jooq.codegen_latest.packages.CWMS_LOC_PACKAGE .call_GET_VERTICAL_DATUM_INFO_LIST(ctx.configuration(), officeId, mask, units); - for (String info : datumInfo) { - VerticalDatumInfo vdi = TimeSeriesDaoImpl.parseVerticalDatumInfo(info); + for (Object info : datumInfo) { + Clob clob = (Clob) info; + VerticalDatumInfo vdi = new XMLv1().parseContent(clob.getAsciiStream(), VerticalDatumInfo.class); resultList.add(vdi); } - return resultList; }); + return new VerticalDatumInfoList(resultList); } public void createVerticalDatumInfo(String officeId, String locationId, VerticalDatumInfo vdi) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java index 004d3604c9..a2b1d3b954 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -141,6 +142,24 @@ private VerticalDatumInfo.Offset[] buildConvertedOffsets(VerticalDatum convertTo return newOffsets.toArray(new VerticalDatumInfo.Offset[]{}); } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + VerticalDatumInfo that = (VerticalDatumInfo) o; + return Objects.equals(office, that.office) && Objects.equals(unit, that.unit) + && Objects.equals(location, that.location) && Objects.equals(nativeDatum, that.nativeDatum) + && Objects.equals(elevation, that.elevation) + && Objects.equals(localDatumName, that.localDatumName) + && Objects.deepEquals(offsets, that.offsets); + } + + @Override + public int hashCode() { + return Objects.hash(office, unit, location, nativeDatum, elevation, localDatumName, Arrays.hashCode(offsets)); + } + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static class Offset { @JacksonXmlProperty(isAttribute = true) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java new file mode 100644 index 0000000000..9e099f5367 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java @@ -0,0 +1,63 @@ +/* + * + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.data.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; +import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv1; +import cwms.cda.formatters.xml.XMLv2; +import java.util.List; + +@JsonRootName("vertical-data") +@JacksonXmlRootElement(localName = "vertical-data") +@FormattableWith(contentType = Formats.XMLV1, formatter = XMLv1.class) +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) +@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) +public final class VerticalDatumInfoList extends CwmsDTOBase { + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "vertical-datum-info") + @JsonProperty("vertical-datum-info") + private final List verticalDatumInfoList; + + @JsonCreator + public VerticalDatumInfoList(@JsonProperty("vertical-datum-info") List verticalDatumInfoList) { + this.verticalDatumInfoList = verticalDatumInfoList; + } + + public List getVerticalDatumInfoList() { + return verticalDatumInfoList; + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java index a65687052b..379e0d5973 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java @@ -28,8 +28,10 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.data.dto.VerticalDatumInfoList; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; @@ -52,10 +54,12 @@ final class VerticalDatumControllerTestIT extends DataApiTestIT { private static final String OFFICE_ID = TestAccounts.KeyUser.SPK_NORMAL.getOperatingOffice(); private static final String TEST_LOCATION = "VDI_LOC_TEST"; + private static final String TEST_LOCATION2 = "VDI_LOC_TEST2"; @BeforeAll static void setup() throws Exception { createLocation(TEST_LOCATION, true, OFFICE_ID); + createLocation(TEST_LOCATION2, true, OFFICE_ID); } @@ -284,8 +288,36 @@ void test_vertical_datum_getAll(ContentType contentType) { .assertThat() .statusCode(is(HttpServletResponse.SC_CREATED)); - // GET + VerticalDatumInfo vdi2 = new VerticalDatumInfo.Builder() + .withOffice(OFFICE_ID) + .withLocation(TEST_LOCATION2) + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(200.0) + .withOffsets(offsets) + .build(); + + vdiPayload = Formats.format(contentType, vdi2); + + // CREATE given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION2 + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // GET + String vdiList = given() .log().ifValidationFails(LogDetail.ALL, true) .accept(contentType.toString()) .queryParam(OFFICE, OFFICE_ID) @@ -298,9 +330,14 @@ void test_vertical_datum_getAll(ContentType contentType) { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .body("size()", Matchers.equalTo(1)) - .body("[0].elevation", Matchers.closeTo(100, 0.001)) - .body("native-datum", Matchers.equalTo("NGVD-29")); + .extract().asString(); + + VerticalDatumInfoList list = Formats.parseContent(contentType, vdiList, VerticalDatumInfoList.class); + + assertEquals(2, list.getVerticalDatumInfoList().size()); + for (VerticalDatumInfo vdiInfo : list.getVerticalDatumInfoList()) { + assertTrue(vdiInfo.equals(vdi) || vdiInfo.equals(vdi2)); + } // DELETE given() @@ -331,6 +368,36 @@ void test_vertical_datum_getAll(ContentType contentType) { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + // DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/location/" + TEST_LOCATION2 + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + //VERIFY DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/" + TEST_LOCATION2 + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } @MethodSource("provideFormats") diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml b/cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml new file mode 100644 index 0000000000..09b3dbce82 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml @@ -0,0 +1,21 @@ + + + + VDI_LOC_TEST2 + NGVD-29 + 200.0 + + NAVD-88 + -0.5 + + + + VDI_LOC_TEST + NGVD-29 + 100.0 + + NAVD-88 + -0.5 + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee7f9781f9..1949354f18 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ jaxb-impl = "3.0.2" jooq-codegen = "26.02.17-RC01-oracle19c" codegen-latest = "latest-dev_sha256_b91430b177ea-oracle19c" codegen-legacy = "24.12.04-2025.01.21" -jooq = "3.18.7-jdk11" +jooq = "03c55e2889779208d011746d8eaf0e34e77f9756" slf4j = "2.0.17" hec-monolith = "3.3.20" hec-nucleus = "2.0.1" From a2afe0f04bf546dd4ec4e6159c80842ed317e591 Mon Sep 17 00:00:00 2001 From: zack-rma Date: Mon, 17 Aug 2026 12:03:13 -0700 Subject: [PATCH 4/4] Added unit test for serialization / deserialization --- .../cda/data/dto/VerticalDatumInfoList.java | 23 +++++- .../api/VerticalDatumControllerTestIT.java | 5 +- .../data/dto/VerticalDatumInfoListTest.java | 80 +++++++++++++++++++ .../{api => data/dto}/vertical-datum-bulk.xml | 8 +- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoListTest.java rename cwms-data-api/src/test/resources/cwms/cda/{api => data/dto}/vertical-datum-bulk.xml (100%) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java index 9e099f5367..b9b8017236 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java @@ -39,6 +39,7 @@ import cwms.cda.formatters.xml.XMLv1; import cwms.cda.formatters.xml.XMLv2; import java.util.List; +import java.util.Objects; @JsonRootName("vertical-data") @JacksonXmlRootElement(localName = "vertical-data") @@ -50,14 +51,28 @@ public final class VerticalDatumInfoList extends CwmsDTOBase { @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "vertical-datum-info") @JsonProperty("vertical-datum-info") - private final List verticalDatumInfoList; + private final List datumList; @JsonCreator public VerticalDatumInfoList(@JsonProperty("vertical-datum-info") List verticalDatumInfoList) { - this.verticalDatumInfoList = verticalDatumInfoList; + this.datumList = verticalDatumInfoList; } - public List getVerticalDatumInfoList() { - return verticalDatumInfoList; + public List getDatumList() { + return datumList; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + VerticalDatumInfoList that = (VerticalDatumInfoList) o; + return Objects.equals(datumList, that.datumList); + } + + @Override + public int hashCode() { + return Objects.hashCode(datumList); } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java index 379e0d5973..3ac2fa5117 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java @@ -39,7 +39,6 @@ import io.restassured.filter.log.LogDetail; import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; -import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -334,8 +333,8 @@ void test_vertical_datum_getAll(ContentType contentType) { VerticalDatumInfoList list = Formats.parseContent(contentType, vdiList, VerticalDatumInfoList.class); - assertEquals(2, list.getVerticalDatumInfoList().size()); - for (VerticalDatumInfo vdiInfo : list.getVerticalDatumInfoList()) { + assertEquals(2, list.getDatumList().size()); + for (VerticalDatumInfo vdiInfo : list.getDatumList()) { assertTrue(vdiInfo.equals(vdi) || vdiInfo.equals(vdi2)); } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoListTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoListTest.java new file mode 100644 index 0000000000..b05a9a47a8 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoListTest.java @@ -0,0 +1,80 @@ +/* + * + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.data.dto; + +import static cwms.cda.data.dao.JsonRatingUtilsTest.readFully; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +final class VerticalDatumInfoListTest { + + @Test + void test_roundtrip() throws Exception { + List list = new ArrayList<>(); + VerticalDatumInfo info = new VerticalDatumInfo.Builder() + .withElevation(100.0) + .withNativeDatum("NGVD-29") + .withUnit("m") + .withLocation("VDI_LOC_TEST") + .withOffice("SPK") + .withOffset(true, "NAVD-88", -0.5) + .build(); + list.add(info); + VerticalDatumInfo info2 = new VerticalDatumInfo.Builder() + .withElevation(200.0) + .withNativeDatum("NGVD-29") + .withUnit("m") + .withOffice("SPK") + .withLocation("VDI_LOC_TEST2") + .withOffset(true, "NAVD-88", -0.5) + .build(); + list.add(info2); + VerticalDatumInfoList vdiList = new VerticalDatumInfoList(list); + String serialized = Formats.format(new ContentType(Formats.XML), vdiList); + InputStream stream = getClass().getClassLoader().getResourceAsStream("cwms/cda/data/dto/vertical-datum-bulk.xml"); + assertNotNull(stream); + String expected = readFully(stream); + VerticalDatumInfoList deserializedFromFile = Formats.parseContent(new ContentType(Formats.XML), expected, VerticalDatumInfoList.class); + VerticalDatumInfoList deserialized = Formats.parseContent(new ContentType(Formats.XML), serialized, VerticalDatumInfoList.class); + for (VerticalDatumInfo vdi : deserialized.getDatumList()) { + assertTrue(vdi.equals(info) || vdi.equals(info2)); + } + for (VerticalDatumInfo vdi : deserializedFromFile.getDatumList()) { + assertTrue(vdi.equals(info) || vdi.equals(info2)); + } + assertEquals(2, deserialized.getDatumList().size()); + assertEquals(2, deserializedFromFile.getDatumList().size()); + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/vertical-datum-bulk.xml similarity index 100% rename from cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml rename to cwms-data-api/src/test/resources/cwms/cda/data/dto/vertical-datum-bulk.xml index 09b3dbce82..793840482e 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/vertical-datum-bulk.xml +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/vertical-datum-bulk.xml @@ -1,18 +1,18 @@ - VDI_LOC_TEST2 + VDI_LOC_TEST NGVD-29 - 200.0 + 100.0 NAVD-88 -0.5 - VDI_LOC_TEST + VDI_LOC_TEST2 NGVD-29 - 100.0 + 200.0 NAVD-88 -0.5