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 0c0e3ccc9..d441b15b2 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; @@ -488,6 +488,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 b3f58a0a3..47c715c82 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 @@ -73,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 2bdc82aa2..319299f88 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,10 +29,13 @@ 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; +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; @@ -41,12 +44,16 @@ 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; import cwms.cda.api.errors.CdaError; import cwms.cda.api.errors.ExceptionTraceSupport; 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; @@ -59,6 +66,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 +76,8 @@ 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 static final String ERROR_MESSAGE = "Failed to process request to retrieve Vertical Datum Info"; private final MetricRegistry metrics; private final Histogram requestResultSize; @@ -81,9 +91,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_SYSTEM, + description = "Specifies the unit system of measure for elevation/offsets (SI or EN). Default is EN.") + }, + responses = { + @OpenApiResponse(status = Controllers.STATUS_200, + 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, + 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 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); + VerticalDatumInfoList vdiList = dao.retrieveVerticalDatumInfoList(office, locationMask, unitSystem); + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfoList.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, vdiList); + 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, + ERROR_MESSAGE, ex); + logger.atSevere().withCause(ex).log(ERROR_MESSAGE); + ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(error); + } } @OpenApi( @@ -117,7 +167,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); @@ -126,49 +175,61 @@ 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); } } @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 1a072dbce..10797d86e 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,11 +26,16 @@ 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; import org.jooq.Record1; import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_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. @@ -51,6 +56,22 @@ public VerticalDatumInfo retrieveVerticalDatumInfo(String officeId, String locat }); } + public VerticalDatumInfoList retrieveVerticalDatumInfoList(String officeId, String locMask, String units) { + List resultList = new ArrayList<>(); + 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 (Object info : datumInfo) { + Clob clob = (Clob) info; + VerticalDatumInfo vdi = new XMLv1().parseContent(clob.getAsciiStream(), VerticalDatumInfo.class); + resultList.add(vdi); + } + }); + return new VerticalDatumInfoList(resultList); + } + public void createVerticalDatumInfo(String officeId, String locationId, VerticalDatumInfo vdi) { connection(dsl, conn -> { DSLContext ctx = getDslContext(conn, officeId); @@ -98,7 +119,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/main/java/cwms/cda/data/dto/VerticalDatumInfo.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java index 004d3604c..a2b1d3b95 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 000000000..b9b801723 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfoList.java @@ -0,0 +1,78 @@ +/* + * + * 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; +import java.util.Objects; + +@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 datumList; + + @JsonCreator + public VerticalDatumInfoList(@JsonProperty("vertical-datum-info") List verticalDatumInfoList) { + this.datumList = 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 3b211002a..9c0c99a7a 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,15 +28,18 @@ 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.MinimumSchema; import fixtures.TestAccounts; import fixtures.TestAccounts.KeyUser; import io.restassured.filter.log.LogDetail; +import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -44,7 +47,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 { @@ -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); } @@ -247,6 +251,156 @@ void test_vertical_datum_crud(ContentType contentType) { .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + @MinimumSchema(260716) + @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)); + + 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) + .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(); + + VerticalDatumInfoList list = Formats.parseContent(contentType, vdiList, VerticalDatumInfoList.class); + + assertEquals(2, list.getDatumList().size()); + for (VerticalDatumInfo vdiInfo : list.getDatumList()) { + assertTrue(vdiInfo.equals(vdi) || vdiInfo.equals(vdi2)); + } + + // 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)); + + // 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") @ParameterizedTest void test_create_vertical_datum_already_exists_fails(ContentType contentType) { @@ -302,6 +456,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) { 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 000000000..b05a9a47a --- /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/data/dto/vertical-datum-bulk.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/vertical-datum-bulk.xml new file mode 100644 index 000000000..793840482 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/vertical-datum-bulk.xml @@ -0,0 +1,21 @@ + + + + VDI_LOC_TEST + NGVD-29 + 100.0 + + NAVD-88 + -0.5 + + + + VDI_LOC_TEST2 + NGVD-29 + 200.0 + + NAVD-88 + -0.5 + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0377e7552..23ebc347b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,9 +2,9 @@ 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" +jooq = "03c55e2889779208d011746d8eaf0e34e77f9756" slf4j = "2.0.17" hec-monolith = "3.3.20" hec-nucleus = "2.0.1"