From c0e61550aee0bd46ebb64ef04655c5e65d3f9b28 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Fri, 10 Oct 2025 15:55:36 -0700 Subject: [PATCH 01/16] Adding controller integration tests for supporting vertical datum. Adding VERTICAL_DATUM constant to the Controllers class for use in the controller implementations. Adding example rating with vertical datum. Adding enum for defining supported vertical datum. --- .../test/java/cwms/cda/api/DataApiTestIT.java | 62 ++++++++++++++ .../api/rating/RatingsControllerTestIT.java | 77 ++++++++++++++++- .../cda/api/vertical_datum_example_rating.xml | 83 +++++++++++++++++++ 3 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml diff --git a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java index 4311d1394..2b42c0d0d 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java @@ -33,6 +33,7 @@ import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.StreamDao; import cwms.cda.data.dao.basin.BasinDao; +import cwms.cda.data.dao.VerticalDatum; import cwms.cda.data.dto.Location; import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; @@ -81,6 +82,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; /** @@ -330,6 +332,66 @@ protected static void createLocation(String location, boolean active, String off }, "cwms_20"); } + protected static void createLocationWithVerticalDatum(String location, boolean active, String office, VerticalDatum verticalDatum) throws SQLException + { + createLocation(location, active, office); + updateLocation(location, active, office, verticalDatum); + } + + private static void updateLocation(String location, boolean active, String officeId, VerticalDatum verticalDatum) throws SQLException { + + String P_LOCATION_ID = location; + String P_LOCATION_TYPE = "SITE"; + Number P_ELEVATION = 11; + String P_ELEV_UNIT_ID = "m"; + + // Pretty sure this isn't supposed to have a dash. The create doesn't check. The default create just passes null. + // If it has a dash then the offsets don't work. + // select VERTICAL_DATUM, count(*) as COUNT + // from AT_PHYSICAL_LOCATION + // group by VERTICAL_DATUM + // order by COUNT desc + // has no entries with a dash in the name (unless we've run this test with a dash). + String P_VERTICAL_DATUM = verticalDatum.toString(); + Number P_LATITUDE = 38.5757; // pretty sure that if these are 0,0 then its not inside the navd88 bounds and the offsets come back [] + Number P_LONGITUDE = -121.4789; + String P_HORIZONTAL_DATUM = "WGS84"; + String P_PUBLIC_NAME = "Integration Test Sac Dam"; + String P_LONG_NAME= null; + String P_DESCRIPTION = "for testing"; + String P_TIME_ZONE_ID = "UTC"; + String P_COUNTY_NAME = "Sacramento"; + String P_STATE_INITIAL = "CA"; + String P_ACTIVE = active ? "T" : "F"; + String P_DB_OFFICE_ID = officeId; + + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + DSLContext dslContext = getDslContext(c, officeId); + + // CWMS_LOC_PACKAGE.call_DELETE_LOCATION(dslContext.configuration(), P_LOCATION_ID, String.valueOf(DeleteRule.DELETE_LOC_CASCADE), P_DB_OFFICE_ID); + // CWMS_LOC_PACKAGE.call_CREATE_LOCATION(dslContext.configuration(), + // P_LOCATION_ID, P_LOCATION_TYPE, P_ELEVATION, P_ELEV_UNIT_ID, P_VERTICAL_DATUM, P_LATITUDE, P_LONGITUDE, + // P_HORIZONTAL_DATUM, P_PUBLIC_NAME, P_LONG_NAME, P_DESCRIPTION, P_TIME_ZONE_ID, P_COUNTY_NAME, P_STATE_INITIAL, + // P_ACTIVE, P_DB_OFFICE_ID); + + String P_IGNORENULLS = "F"; + CWMS_LOC_PACKAGE.call_UPDATE_LOCATION(dslContext.configuration(), + P_LOCATION_ID, P_LOCATION_TYPE, P_ELEVATION, P_ELEV_UNIT_ID, P_VERTICAL_DATUM, P_LATITUDE, P_LONGITUDE, + P_HORIZONTAL_DATUM, P_PUBLIC_NAME, P_LONG_NAME, P_DESCRIPTION, P_TIME_ZONE_ID, P_COUNTY_NAME, P_STATE_INITIAL, + P_ACTIVE, P_IGNORENULLS, P_DB_OFFICE_ID ); + + }); + + } + + private static DSLContext getDslContext(Connection database, String officeId) + { + DSLContext dsl = DSL.using(database, SQLDialect.ORACLE18C); + CWMS_ENV_PACKAGE.call_SET_SESSION_OFFICE_ID(dsl.configuration(), officeId); + return dsl; + } + /** * Creates a location saving the data for later deletion. With the following defaults: * diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java index 3a956f82e..f7d12c202 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java @@ -26,8 +26,12 @@ import cwms.cda.api.DataApiTestIT; import cwms.cda.data.dao.JooqDao; +import cwms.cda.data.dao.JsonRatingUtils; +import cwms.cda.data.dao.VerticalDatum; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; +import hec.data.cwmsRating.AbstractRating; +import hec.data.cwmsRating.RatingSet; import hec.data.cwmsRating.io.RatingSetContainer; import hec.data.cwmsRating.io.RatingSpecContainer; import io.restassured.filter.log.LogDetail; @@ -36,6 +40,8 @@ import mil.army.usace.hec.cwms.rating.io.xml.RatingContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import mil.army.usace.hec.metadata.VerticalDatumContainer; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -47,8 +53,7 @@ import static cwms.cda.api.Controllers.*; 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; @Tag("integration") class RatingsControllerTestIT extends DataApiTestIT @@ -88,7 +93,7 @@ static void cleanUp() static void store(boolean storeTemplate) throws Exception { //Make sure we always have something. - createLocation(EXISTING_LOC, true, SPK); + createLocationWithVerticalDatum(EXISTING_LOC, true, SPK, VerticalDatum.NAVD88); String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); ratingXml = ratingXml.replaceAll("Zanesville", EXISTING_LOC); @@ -417,5 +422,71 @@ void test_1206_rating_create_xml() throws IOException { .statusCode(is(HttpServletResponse.SC_CREATED)); } + @Test + void test_store_vertical_datum() throws Exception + { + String xml = readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml"); + xml = xml.replace("{office-id}", SPK).replace("{location}", EXISTING_LOC); + RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); + AbstractRating originalRating = originalRatingSet.getRatings()[0]; + VerticalDatumContainer originalVerticalDatumContainer = originalRating.getVerticalDatumContainer(); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .body(xml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.XMLV2)) + .extract(); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(BEGIN, "2000-01-01T00:00:00Z") + .queryParam(END, "2100-01-01T00:00:00Z") + .queryParam(VERTICAL_DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); + AbstractRating receivedRating = receivedRatingSet.getRatings()[0]; + + VerticalDatumContainer receivedVerticalDatumContainer = receivedRating.getVerticalDatumContainer(); + assertNotNull(receivedVerticalDatumContainer); + assertEquals(originalVerticalDatumContainer, receivedVerticalDatumContainer); + } } diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml b/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml new file mode 100644 index 000000000..ef7989392 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml @@ -0,0 +1,83 @@ + + + + Elev;Area + Standard + + + Elev + LINEAR + NEAREST + NEAREST + + + Area + + + + {location}.Elev;Area.Standard.Production + Elev;Area.Standard + {location} + Production + + LINEAR + NEAREST + NEAREST + true + true + true + true + + 2222233332 + + 2222233332 + + + + {location}.Elev;Area.Standard.Production + + NGVD-29 + 612.99 + + NGVD-29 + 0.0 + + + NAVD-88 + 2.393 + + + ft;acre + 2016-09-06T20:08:00Z + + 2016-09-06T20:08:00Z + true + + + + 620.0 + 0.0 + + + 621.0 + 1.0 + + + 622.0 + 2.0 + + + 623.0 + 3.0 + + + 624.0 + 4.0 + + + 625.0 + 5.0 + + + + From 69b503c83c7e2898b4d2179de522425337bde6cb Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Fri, 10 Oct 2025 15:56:22 -0700 Subject: [PATCH 02/16] Adding missing controllers and vertical datum enum --- cwms-data-api/src/main/java/cwms/cda/api/Controllers.java | 1 + 1 file changed, 1 insertion(+) 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 3528a22e7..d68bee3d4 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 @@ -96,6 +96,7 @@ public final class Controllers { public static final String NAME = "name"; public static final String CASCADE_DELETE = "cascade-delete"; public static final String DATUM = "datum"; + public static final String VERTICAL_DATUM = "vertical-datum"; public static final String SINCE = "since"; public static final String BEGIN = "begin"; public static final String END = "end"; From f80646572fd7d5939f68791e2bb9028302bd4fb2 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Tue, 14 Oct 2025 09:58:13 -0700 Subject: [PATCH 03/16] Refactor `RatingsControllerTestIT` by moving vertical datum tests to new `RatingsControllerTestVerticalDatumIT`. Enhance `RatingController` to handle vertical datum conversions. --- .../cwms/cda/api/rating/RatingController.java | 32 ++- .../api/rating/RatingsControllerTestIT.java | 87 +------ .../RatingsControllerTestVerticalDatumIT.java | 229 ++++++++++++++++++ 3 files changed, 266 insertions(+), 82 deletions(-) create mode 100644 cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 691859fb9..1d939f6ad 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -62,6 +62,7 @@ import cwms.cda.data.dao.JsonRatingUtils; import cwms.cda.data.dao.RatingDao; import cwms.cda.data.dao.RatingSetDao; +import cwms.cda.data.dao.VerticalDatum; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.StatusResponse; import cwms.cda.formatters.ContentType; @@ -71,7 +72,9 @@ import cwms.cda.formatters.xml.XMLv2; import cwms.cda.helpers.DateUtils; import hec.data.RatingException; +import hec.data.cwmsRating.AbstractRating; import hec.data.cwmsRating.RatingSet; +import hec.data.cwmsRating.TableRating; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.core.validation.JavalinValidation; @@ -265,7 +268,9 @@ public void delete(@NotNull Context ctx, @NotNull String ratingSpecId) { + "\n* `NAVD88` The elevation values will in the " + "specified or default units above the NAVD-88 datum." + "\n* `NGVD29` The elevation values will be in the " - + "specified or default units above the NGVD-29 datum."), + + "specified or default units above the NGVD-29 datum." + + "\n* `NATIVE` The elevation values will be in the " + + "Location's native datum."), @OpenApiParam(name = AT, description = "Specifies the " + "start of the time window for data to be included in the response. " + "If this field is not specified, any required time window begins 24" @@ -362,6 +367,16 @@ public void getAll(@NotNull Context ctx) { @OpenApiParam(name = METHOD, description = "Specifies " + "the retrieval method used. If no method is provided EAGER will be used.", type = RatingSet.DatabaseLoadMethod.class), + @OpenApiParam(name = DATUM, description = "Specifies the " + + "elevation datum of the response. This field affects only elevation" + + " Ratings. Valid values for this field are:" + + "\n* `NAVD88` The elevation values will in the " + + "specified or default units above the NAVD-88 datum." + + "\n* `NGVD29` The elevation values will be in the " + + "specified or default units above the NGVD-29 datum." + + "\n* `NATIVE` The elevation values will be in the " + + "Location's native datum.", + type = VerticalDatum.class), }, responses = { @OpenApiResponse(status = STATUS_200, content = { @@ -377,6 +392,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) { try (final Timer.Context ignored = markAndTime(GET_ONE)) { String officeId = ctx.queryParam(OFFICE); String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC"); + VerticalDatum verticalDatum = ctx.queryParamAsClass(DATUM, VerticalDatum.class).get(); Instant beginInstant = null; String begin = ctx.queryParam(BEGIN); @@ -394,7 +410,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) { RatingSet.DatabaseLoadMethod.class) .getOrDefault(RatingSet.DatabaseLoadMethod.EAGER); - String body = getRatingSetString(ctx, method, officeId, rating, beginInstant, endInstant); + String body = getRatingSetString(ctx, method, officeId, rating, beginInstant, endInstant, verticalDatum); if (body != null) { ctx.result(body); ctx.status(HttpCode.OK); @@ -406,7 +422,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) { @Nullable private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod method, String officeId, String rating, Instant begin, - Instant end) { + Instant end, VerticalDatum verticalDatum) { String retval = null; try (final Timer.Context ignored = markAndTime("getRatingSetString")) { @@ -421,6 +437,16 @@ private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod meth try { RatingSet ratingSet = getRatingSet(ctx, method, officeId, rating, begin, end); if (ratingSet != null) { + //Apply vertical datum conversion if needed + if (verticalDatum != null) { + for (AbstractRating temp : ratingSet.getRatings()) + { + if (temp instanceof TableRating) + { + + } + } + } if (isJson) { retval = JsonRatingUtils.toJson(ratingSet); } else { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java index f7d12c202..3092c3ac2 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java @@ -26,34 +26,30 @@ import cwms.cda.api.DataApiTestIT; import cwms.cda.data.dao.JooqDao; -import cwms.cda.data.dao.JsonRatingUtils; import cwms.cda.data.dao.VerticalDatum; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; -import hec.data.cwmsRating.AbstractRating; -import hec.data.cwmsRating.RatingSet; import hec.data.cwmsRating.io.RatingSetContainer; import hec.data.cwmsRating.io.RatingSpecContainer; import io.restassured.filter.log.LogDetail; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.cwms.rating.io.xml.RatingContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; -import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; -import mil.army.usace.hec.metadata.VerticalDatumContainer; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; - -import javax.servlet.http.HttpServletResponse; - -import java.io.IOException; - import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; @Tag("integration") class RatingsControllerTestIT extends DataApiTestIT @@ -421,72 +417,5 @@ void test_1206_rating_create_xml() throws IOException { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_CREATED)); } - - @Test - void test_store_vertical_datum() throws Exception - { - String xml = readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml"); - xml = xml.replace("{office-id}", SPK).replace("{location}", EXISTING_LOC); - RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); - - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; - String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); - AbstractRating originalRating = originalRatingSet.getRatings()[0]; - VerticalDatumContainer originalVerticalDatumContainer = originalRating.getVerticalDatumContainer(); - - given() - .log().ifValidationFails(LogDetail.ALL,true) - .contentType(Formats.XMLV2) - .body(xml) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, SPK) - .when() - .redirects().follow(true) - .redirects().max(3) - .post("/ratings") - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_CREATED)); - - ExtractableResponse response = given() - .log().ifValidationFails(LogDetail.ALL,true) - .contentType(Formats.XMLV2) - .queryParam(OFFICE, SPK) - .when() - .redirects().follow(true) - .redirects().max(3) - .get("/ratings/" + ratingId) - .then() - .log().ifValidationFails(LogDetail.ALL,true) - .assertThat() - .statusCode(is(HttpServletResponse.SC_OK)) - .contentType(is(Formats.XMLV2)) - .extract(); - - given() - .log().ifValidationFails(LogDetail.ALL,true) - .contentType(Formats.XMLV2) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, SPK) - .queryParam(BEGIN, "2000-01-01T00:00:00Z") - .queryParam(END, "2100-01-01T00:00:00Z") - .queryParam(VERTICAL_DATUM, VerticalDatum.NAVD88) - .when() - .redirects().follow(true) - .redirects().max(3) - .delete("/ratings/" + ratingId) - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); - - RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); - AbstractRating receivedRating = receivedRatingSet.getRatings()[0]; - - VerticalDatumContainer receivedVerticalDatumContainer = receivedRating.getVerticalDatumContainer(); - assertNotNull(receivedVerticalDatumContainer); - assertEquals(originalVerticalDatumContainer, receivedVerticalDatumContainer); - } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java new file mode 100644 index 000000000..73f6c0610 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java @@ -0,0 +1,229 @@ +/* + * MIT License + * Copyright (c) 2025 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.api.rating; + +import cwms.cda.api.DataApiTestIT; +import cwms.cda.data.dao.JooqDao; +import cwms.cda.data.dao.VerticalDatum; +import cwms.cda.formatters.Formats; +import fixtures.TestAccounts; +import hec.data.cwmsRating.AbstractRating; +import hec.data.cwmsRating.RatingSet; +import hec.data.cwmsRating.io.RatingSetContainer; +import hec.data.cwmsRating.io.RatingSpecContainer; +import io.restassured.filter.log.LogDetail; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import mil.army.usace.hec.metadata.VerticalDatumContainer; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import static cwms.cda.api.Controllers.*; +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.assertNotNull; + +@Tag("integration") +class RatingsControllerTestVerticalDatumIT extends DataApiTestIT +{ + static final String EXISTING_LOC = "RatingsControllerTestIT"; + static final String TEMPLATE = EXISTING_LOC + ".Elev;Area.Standard"; + static final String SPK = "SPK"; + static final VerticalDatum LOCATION_VERTICAL_DATUM = VerticalDatum.NAVD88; + + @BeforeAll + static void beforeAll() throws Exception + { + //Make sure we always have something. + createLocationWithVerticalDatum(EXISTING_LOC, true, SPK, LOCATION_VERTICAL_DATUM); + + String xml = readVerticalDatumRatingXml(); + RatingSetContainer container = RatingSetContainerXmlFactory.ratingSetContainerFromXml(xml); + RatingSpecContainer specContainer = container.ratingSpecContainer; + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); + + //Create Template + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Create Spec + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + @AfterAll + static void cleanUp() + { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Delete Template + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/template/" + TEMPLATE) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + @EnumSource(value = TestLocationVerticalDatumData.class) + @ParameterizedTest + void test_store_vertical_datum_null_vd_null_create(TestLocationVerticalDatumData testData) throws Exception + { + String xml = readVerticalDatumRatingXml(); + RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); + AbstractRating originalRating = originalRatingSet.getRatings()[0]; + originalRating.setVerticalDatumContainer(null); + VerticalDatum storedVerticalDatum = null; + + storeRatingFromXml(xml, user, storedVerticalDatum); + + String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "NULL" : testData._requestedVerticalDatum.toString(); + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .queryParam(VERTICAL_DATUM, requestedVerticalDatum) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.XMLV2)) + .extract(); + + deleteRatingEffectiveDates(user, ratingId); + + RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); + AbstractRating receivedRating = receivedRatingSet.getRatings()[0]; + + VerticalDatumContainer receivedVerticalDatumContainer = receivedRating.getVerticalDatumContainer(); + assertNotNull(receivedVerticalDatumContainer); + assertEquals(testData._expectedVerticalDatum, receivedVerticalDatumContainer.getCurrentVerticalDatum()); + } + + private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user, VerticalDatum storedVerticalDatum) { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, storedVerticalDatum) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + private static void deleteRatingEffectiveDates(TestAccounts.KeyUser user, String ratingId) { + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(BEGIN, "2000-01-01T00:00:00Z") + .queryParam(END, "2100-01-01T00:00:00Z") + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + private static @NotNull String readVerticalDatumRatingXml() throws IOException { + return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml") + .replace("{office-id}", SPK).replace("{location}", EXISTING_LOC); + } + + private enum TestLocationVerticalDatumData + { + NULL("NAVD-88", null), + NATIVE("NAVD-88", VerticalDatum.NATIVE), + NAVD88("NAVD-88", VerticalDatum.NAVD88), + NGVD29("NGVD-29", VerticalDatum.NGVD29), + ; + + final VerticalDatum _requestedVerticalDatum; + final String _expectedVerticalDatum; + + TestLocationVerticalDatumData(String expectedVerticalDatum, VerticalDatum requestedVerticalDatum) + { + _expectedVerticalDatum = expectedVerticalDatum; + _requestedVerticalDatum = requestedVerticalDatum; + } + } +} From 95079de25eafb0602a254f35a9ab368ab79e07d2 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Fri, 17 Oct 2025 10:22:28 -0700 Subject: [PATCH 04/16] Improve vertical datum handling in `VerticalDatum` and update `RatingsControllerTestVerticalDatumIT` to include additional test cases for datum combinations. Enhance `RatingController` to log vertical datum conversion failures gracefully. --- .../cwms/cda/api/rating/RatingController.java | 23 ++- .../java/cwms/cda/data/dao/VerticalDatum.java | 2 +- .../RatingsControllerTestVerticalDatumIT.java | 192 +++++++++++------- 3 files changed, 139 insertions(+), 78 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 1d939f6ad..c305e4243 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -92,6 +92,7 @@ import javax.servlet.http.HttpServletResponse; import javax.xml.transform.TransformerException; import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import mil.army.usace.hec.metadata.VerticalDatumException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jooq.DSLContext; @@ -392,7 +393,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) { try (final Timer.Context ignored = markAndTime(GET_ONE)) { String officeId = ctx.queryParam(OFFICE); String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC"); - VerticalDatum verticalDatum = ctx.queryParamAsClass(DATUM, VerticalDatum.class).get(); + VerticalDatum verticalDatum = VerticalDatum.getVerticalDatum(ctx.queryParam(DATUM)); Instant beginInstant = null; String begin = ctx.queryParam(BEGIN); @@ -439,12 +440,24 @@ private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod meth if (ratingSet != null) { //Apply vertical datum conversion if needed if (verticalDatum != null) { - for (AbstractRating temp : ratingSet.getRatings()) - { - if (temp instanceof TableRating) + try { + switch (verticalDatum) { - + case NAVD88: + ratingSet.toNAVD88(); + break; + case NGVD29: + ratingSet.toNGVD29(); + break; + case NATIVE: + ratingSet.toNativeVerticalDatum(); + break; + default: + logger.log(Level.SEVERE, "Unknown vertical datum: " + verticalDatum); + break; } + } catch (VerticalDatumException vde) { + logger.log(Level.WARNING, vde, () -> "Failed to convert rating " + rating + " to requested vertical datum: " + verticalDatum); } } if (isJson) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java index 39aaef9b4..16d1b40ab 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java @@ -15,7 +15,7 @@ public enum VerticalDatum { public static VerticalDatum getVerticalDatum(String input) { VerticalDatum retval = null; - if (input != null) { + if (input != null && !input.isBlank()) { input = input.replace("-", ""); retval = VerticalDatum.valueOf(input.toUpperCase()); } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java index 73f6c0610..2990af110 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java @@ -33,6 +33,7 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import java.io.IOException; +import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; @@ -43,7 +44,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; @@ -51,86 +53,98 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; @Tag("integration") -class RatingsControllerTestVerticalDatumIT extends DataApiTestIT -{ - static final String EXISTING_LOC = "RatingsControllerTestIT"; - static final String TEMPLATE = EXISTING_LOC + ".Elev;Area.Standard"; +class RatingsControllerTestVerticalDatumIT extends DataApiTestIT { + static final String BASE_LOCATION = "RatingDatumTest"; + static final String LOC_WITH_NAVD88 = BASE_LOCATION + "-NAVD88"; + static final String LOC_WITH_NGVD29 = BASE_LOCATION + "-NGVD29"; + static final String TEMPLATE = "Elev;Area.Standard"; static final String SPK = "SPK"; - static final VerticalDatum LOCATION_VERTICAL_DATUM = VerticalDatum.NAVD88; @BeforeAll - static void beforeAll() throws Exception - { + static void beforeAll() throws Exception { //Make sure we always have something. - createLocationWithVerticalDatum(EXISTING_LOC, true, SPK, LOCATION_VERTICAL_DATUM); + createLocation(BASE_LOCATION, true, SPK); + createLocationWithVerticalDatum(LOC_WITH_NAVD88, true, SPK, VerticalDatum.NAVD88); + createLocationWithVerticalDatum(LOC_WITH_NGVD29, true, SPK, VerticalDatum.NGVD29); - String xml = readVerticalDatumRatingXml(); + String xml = readVerticalDatumRatingXml(BASE_LOCATION); RatingSetContainer container = RatingSetContainerXmlFactory.ratingSetContainerFromXml(xml); RatingSpecContainer specContainer = container.ratingSpecContainer; String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); - //Create Template + createTemplate(templateXml, user); + + createSpec(specXml, user); + } + + private static void createSpec(String specXml, TestAccounts.KeyUser user) { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + private static void createTemplate(String templateXml, TestAccounts.KeyUser user) { given() - .log().ifValidationFails(LogDetail.ALL,true) + .log().ifValidationFails(LogDetail.ALL, true) .contentType(Formats.XMLV2) .body(templateXml) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, SPK) .when() - .redirects().follow(true) - .redirects().max(3) + .redirects() + .follow(true) + .redirects() + .max(3) .post("/ratings/template") .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) .statusCode(is(HttpServletResponse.SC_CREATED)); - - //Create Spec - given() - .log().ifValidationFails(LogDetail.ALL,true) - .contentType(Formats.XMLV2) - .body(specXml) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, SPK) - .when() - .redirects().follow(true) - .redirects().max(3) - .post("/ratings/spec") - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_CREATED)); } @AfterAll - static void cleanUp() - { + static void cleanUp() { TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; // Delete Template given() - .log().ifValidationFails(LogDetail.ALL,true) + .log().ifValidationFails(LogDetail.ALL, true) .contentType(Formats.XMLV2) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, SPK) .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) .when() - .redirects().follow(true) - .redirects().max(3) + .redirects() + .follow(true) + .redirects() + .max(3) .delete("/ratings/template/" + TEMPLATE) .then() - .log().ifValidationFails(LogDetail.ALL,true) + .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } - @EnumSource(value = TestLocationVerticalDatumData.class) + @MethodSource(value = "provideDatumCombinations") @ParameterizedTest - void test_store_vertical_datum_null_vd_null_create(TestLocationVerticalDatumData testData) throws Exception - { - String xml = readVerticalDatumRatingXml(); + void test_store_vertical_datum_null_vd_null_create(TestLocationIds locId, + TestLocationVerticalDatumData testData) throws Exception { + String xml = readVerticalDatumRatingXml(locId._locationId); RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; @@ -141,19 +155,21 @@ void test_store_vertical_datum_null_vd_null_create(TestLocationVerticalDatumData storeRatingFromXml(xml, user, storedVerticalDatum); - String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "NULL" : testData._requestedVerticalDatum.toString(); + String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); ExtractableResponse response = given() - .log().ifValidationFails(LogDetail.ALL,true) + .log().ifValidationFails(LogDetail.ALL, true) .contentType(Formats.XMLV2) .queryParam(OFFICE, SPK) - .queryParam(VERTICAL_DATUM, requestedVerticalDatum) + .queryParam(DATUM, requestedVerticalDatum) .when() - .redirects().follow(true) - .redirects().max(3) + .redirects() + .follow(true) + .redirects() + .max(3) .get("/ratings/" + ratingId) .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) .statusCode(is(HttpServletResponse.SC_OK)) .contentType(is(Formats.XMLV2)) .extract(); @@ -165,7 +181,17 @@ void test_store_vertical_datum_null_vd_null_create(TestLocationVerticalDatumData VerticalDatumContainer receivedVerticalDatumContainer = receivedRating.getVerticalDatumContainer(); assertNotNull(receivedVerticalDatumContainer); - assertEquals(testData._expectedVerticalDatum, receivedVerticalDatumContainer.getCurrentVerticalDatum()); + + VerticalDatum expectedDatum = testData._requestedVerticalDatum; + + if (testData._requestedVerticalDatum == VerticalDatum.NATIVE) { + expectedDatum = locId._nativeDatum; + } + + VerticalDatum receivedDatum = VerticalDatum.getVerticalDatum( + receivedVerticalDatumContainer.getCurrentVerticalDatum()); + + assertEquals(expectedDatum, receivedDatum); } private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user, VerticalDatum storedVerticalDatum) { @@ -177,52 +203,74 @@ private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user, Ve .queryParam(OFFICE, SPK) .queryParam(DATUM, storedVerticalDatum) .when() - .redirects().follow(true) - .redirects().max(3) + .redirects() + .follow(true) + .redirects() + .max(3) .post("/ratings") .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) .statusCode(is(HttpServletResponse.SC_CREATED)); } private static void deleteRatingEffectiveDates(TestAccounts.KeyUser user, String ratingId) { given() - .log().ifValidationFails(LogDetail.ALL,true) + .log().ifValidationFails(LogDetail.ALL, true) .contentType(Formats.XMLV2) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, SPK) .queryParam(BEGIN, "2000-01-01T00:00:00Z") .queryParam(END, "2100-01-01T00:00:00Z") .when() - .redirects().follow(true) - .redirects().max(3) + .redirects() + .follow(true) + .redirects() + .max(3) .delete("/ratings/" + ratingId) .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } - private static @NotNull String readVerticalDatumRatingXml() throws IOException { - return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml") - .replace("{office-id}", SPK).replace("{location}", EXISTING_LOC); + private static Stream provideDatumCombinations() { + return Stream.of(TestLocationIds.values()) + .flatMap(locId -> Stream.of(TestLocationVerticalDatumData.values()) + .map(datum -> Arguments.of(locId, datum))); + } + + + private static @NotNull String readVerticalDatumRatingXml(String location) throws IOException { + return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml").replace("{office-id}", SPK) + .replace("{location}", location); + } + + private enum TestLocationIds { + BASE(BASE_LOCATION, null), + NAVD88(LOC_WITH_NAVD88, VerticalDatum.NAVD88), + NGVD29(LOC_WITH_NGVD29, VerticalDatum.NGVD29), + ; + + final String _locationId; + final VerticalDatum _nativeDatum; + + TestLocationIds(String locationId, VerticalDatum nativeDatum) { + _locationId = locationId; + _nativeDatum = nativeDatum; + } } - private enum TestLocationVerticalDatumData - { - NULL("NAVD-88", null), - NATIVE("NAVD-88", VerticalDatum.NATIVE), - NAVD88("NAVD-88", VerticalDatum.NAVD88), - NGVD29("NGVD-29", VerticalDatum.NGVD29), + private enum TestLocationVerticalDatumData { + NULL(null), + NATIVE(VerticalDatum.NATIVE), + NAVD88(VerticalDatum.NAVD88), + NGVD29(VerticalDatum.NGVD29), ; final VerticalDatum _requestedVerticalDatum; - final String _expectedVerticalDatum; - TestLocationVerticalDatumData(String expectedVerticalDatum, VerticalDatum requestedVerticalDatum) - { - _expectedVerticalDatum = expectedVerticalDatum; + TestLocationVerticalDatumData(VerticalDatum requestedVerticalDatum) { _requestedVerticalDatum = requestedVerticalDatum; } } From d4ff58bc4de20271c3026b6d331db65d5f830e7f Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Mon, 20 Oct 2025 13:21:31 -0700 Subject: [PATCH 05/16] Trim input string in `VerticalDatum` and expand test cases in `RatingsControllerTestVerticalDatumIT` to cover additional vertical datum combinations and query scenarios. --- .../java/cwms/cda/data/dao/VerticalDatum.java | 2 +- .../RatingsControllerTestVerticalDatumIT.java | 91 +++++++++++++++++-- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java index 16d1b40ab..c23e573ed 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java @@ -16,7 +16,7 @@ public static VerticalDatum getVerticalDatum(String input) { VerticalDatum retval = null; if (input != null && !input.isBlank()) { - input = input.replace("-", ""); + input = input.trim().replace("-", ""); retval = VerticalDatum.valueOf(input.toUpperCase()); } return retval; diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java index 2990af110..9de4b9f90 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java @@ -50,7 +50,6 @@ 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.assertNotNull; @Tag("integration") class RatingsControllerTestVerticalDatumIT extends DataApiTestIT { @@ -142,8 +141,9 @@ static void cleanUp() { @MethodSource(value = "provideDatumCombinations") @ParameterizedTest - void test_store_vertical_datum_null_vd_null_create(TestLocationIds locId, - TestLocationVerticalDatumData testData) throws Exception { + void test_vertical_datum_get_all(TestLocationIds locId, TestLocationVerticalDatumData testData) throws Exception { + //This tests getting a rating with various combinations of native location datum and requested datum + //Storing a rating without any vertical datum info, then requesting it back with various datum requests String xml = readVerticalDatumRatingXml(locId._locationId); RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); @@ -155,6 +155,65 @@ void test_store_vertical_datum_null_vd_null_create(TestLocationIds locId, storeRatingFromXml(xml, user, storedVerticalDatum); + //Request the one rating id we stored, using the getAll endpoint with a query param filter + String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, requestedVerticalDatum) + .queryParam(NAME, ratingId) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .get("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.XMLV2)) + .extract(); + + deleteRatingEffectiveDates(user, ratingId); + + RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); + VerticalDatumContainer receivedDatumContainer = receivedRatingSet.getVerticalDatumContainer(); + assertEquals(locId._nativeDatum == null, receivedDatumContainer == null, "Received VerticalDatumContainer presence mismatch. Expected " + (locId._nativeDatum == null ? "null" : "not null")); + + + VerticalDatum expectedDatum = testData._requestedVerticalDatum; + + if (testData._requestedVerticalDatum == VerticalDatum.NATIVE || testData._requestedVerticalDatum == null || locId._nativeDatum == null) { + expectedDatum = locId._nativeDatum; + } + + VerticalDatum receivedDatum = null; + if (receivedDatumContainer != null) { + receivedDatum = VerticalDatum.getVerticalDatum(receivedDatumContainer.getCurrentVerticalDatum()); + } + + assertEquals(expectedDatum, receivedDatum, "Unexpected Current Vertical Datum received"); + } + + @MethodSource(value = "provideDatumCombinations") + @ParameterizedTest + void test_vertical_datum_get_one(TestLocationIds locId, TestLocationVerticalDatumData testData) throws Exception { + //This tests getting a rating with various combinations of native location datum and requested datum + //Storing a rating without any vertical datum info, then requesting it back with various datum requests + String xml = readVerticalDatumRatingXml(locId._locationId); + RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); + AbstractRating originalRating = originalRatingSet.getRatings()[0]; + originalRating.setVerticalDatumContainer(null); + VerticalDatum storedVerticalDatum = null; + + storeRatingFromXml(xml, user, storedVerticalDatum); + + //Use getOne endpoint to get the rating we just stored String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); ExtractableResponse response = given() .log().ifValidationFails(LogDetail.ALL, true) @@ -177,21 +236,22 @@ void test_store_vertical_datum_null_vd_null_create(TestLocationIds locId, deleteRatingEffectiveDates(user, ratingId); RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); - AbstractRating receivedRating = receivedRatingSet.getRatings()[0]; + VerticalDatumContainer receivedDatumContainer = receivedRatingSet.getVerticalDatumContainer(); + assertEquals(locId._nativeDatum == null, receivedDatumContainer == null, "Received VerticalDatumContainer presence mismatch. Expected " + (locId._nativeDatum == null ? "null" : "not null")); - VerticalDatumContainer receivedVerticalDatumContainer = receivedRating.getVerticalDatumContainer(); - assertNotNull(receivedVerticalDatumContainer); VerticalDatum expectedDatum = testData._requestedVerticalDatum; - if (testData._requestedVerticalDatum == VerticalDatum.NATIVE) { + if (testData._requestedVerticalDatum == VerticalDatum.NATIVE || testData._requestedVerticalDatum == null || locId._nativeDatum == null) { expectedDatum = locId._nativeDatum; } - VerticalDatum receivedDatum = VerticalDatum.getVerticalDatum( - receivedVerticalDatumContainer.getCurrentVerticalDatum()); + VerticalDatum receivedDatum = null; + if (receivedDatumContainer != null) { + receivedDatum = VerticalDatum.getVerticalDatum(receivedDatumContainer.getCurrentVerticalDatum()); + } - assertEquals(expectedDatum, receivedDatum); + assertEquals(expectedDatum, receivedDatum, "Unexpected Current Vertical Datum received"); } private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user, VerticalDatum storedVerticalDatum) { @@ -235,6 +295,17 @@ private static void deleteRatingEffectiveDates(TestAccounts.KeyUser user, String } private static Stream provideDatumCombinations() { + //This provides information for 3 locations: + // - BASE_LOCATION: no vertical datum + // - LOC_WITH_NAVD88: native datum NAVD88 + // - LOC_WITH_NGVD29: native datum NGVD29 + //And for each location, we test requesting: + // - null + // - NATIVE + // - NAVD88 + // - NGVD29 + // + //This creates a 3 x 4 matrix of test cases to cover all combinations of these parameters return Stream.of(TestLocationIds.values()) .flatMap(locId -> Stream.of(TestLocationVerticalDatumData.values()) .map(datum -> Arguments.of(locId, datum))); From 528b7a7e59fe129e0f6398a861d83b738184f578 Mon Sep 17 00:00:00 2001 From: Ryan Miles Date: Tue, 30 Dec 2025 12:21:57 -0800 Subject: [PATCH 06/16] Add `DeleteMeTest` for testing vertical datum rating conversions and assertions. --- .../cwms/cda/api/rating/DeleteMeTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java new file mode 100644 index 000000000..a12500618 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java @@ -0,0 +1,51 @@ +//package cwms.cda.api.rating; +// +//import cwms.cda.data.dao.VerticalDatum; +//import hec.data.cwmsRating.RatingSet; +//import hec.data.cwmsRating.io.RatingSetContainer; +//import java.io.File; +//import java.io.IOException; +//import java.net.URL; +//import java.nio.file.Files; +//import java.nio.file.Path; +//import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; +//import org.jetbrains.annotations.NotNull; +//import org.junit.jupiter.api.Test; +//import static org.junit.jupiter.api.Assertions.*; +// +//public class DeleteMeTest { +// static final String BASE_LOCATION = "RatingDatumTest"; +// static final String LOC_WITH_NAVD88 = BASE_LOCATION + "-NAVD88"; +// static final String LOC_WITH_NGVD29 = BASE_LOCATION + "-NGVD29"; +// +// protected static String readResourceFile(String resourcePath) throws IOException { +// URL resource = DeleteMeTest.class.getClassLoader().getResource(resourcePath); +// if (resource == null) { +// throw new IOException("Resource not found: " + resourcePath); +// } +// Path path = new File(resource.getFile()).toPath(); +// return String.join("\n", Files.readAllLines(path)); +// } +// +// static @NotNull String readVerticalDatumRatingXml(String location) throws IOException { +// return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml").replace("{office-id}", "SPK") +// .replace("{location}", location); +// } +// +// @Test +// void test() throws Exception { +// String xml = readVerticalDatumRatingXml(LOC_WITH_NGVD29); +// RatingSetContainer container = RatingSetContainerXmlFactory.ratingSetContainerFromXml(xml); +// RatingSet rs = new RatingSet(container); +// RatingSet rsNew = new RatingSet(container); +// +// assertTrue(rs.toNAVD88()); +// assertNotEquals(rs.getRatings()[0].getRatingExtents()[0][0], rsNew.getRatings()[0].getRatingExtents()[0][0], 0.01); +// +// VerticalDatum vd = VerticalDatum.getVerticalDatum(rs.getNativeVerticalDatum()); +// assertEquals(VerticalDatum.NGVD29, vd); +// +// vd = VerticalDatum.getVerticalDatum(rs.getCurrentVerticalDatum()); +// assertEquals(VerticalDatum.NAVD88, vd); +// } +//} From fa76748bd05097f4222bccd9c79ebf8584b0ab27 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Mon, 5 Jan 2026 15:33:27 -0800 Subject: [PATCH 07/16] CDA-46 - Implements vertical datum support for create, update, and retrieval. Adds integration tests. --- .../cwms/cda/api/rating/RatingController.java | 53 ++- .../main/java/cwms/cda/data/dao/JooqDao.java | 30 ++ .../java/cwms/cda/data/dao/RatingDao.java | 12 +- .../java/cwms/cda/data/dao/RatingSetDao.java | 60 ++- .../dao/RatingsVerticalDatumExtractor.java | 60 +++ .../cwms/cda/data/dao/TimeSeriesDaoImpl.java | 30 -- .../cwms/cda/data/dto/VerticalDatumInfo.java | 30 +- .../cwms/cda/api/rating/DeleteMeTest.java | 51 --- .../RatingsControllerTestVerticalDatumIT.java | 375 +++++++++++++++--- .../RatingsVerticalDatumExtractorTest.java | 20 + .../cda/api/vertical_datum_example_rating.xml | 4 +- gradle/libs.versions.toml | 2 +- 12 files changed, 535 insertions(+), 192 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java delete mode 100644 cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index c305e4243..075a0dfd1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -57,14 +57,17 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.JsonRatingUtils; import cwms.cda.data.dao.RatingDao; import cwms.cda.data.dao.RatingSetDao; +import cwms.cda.data.dao.RatingsVerticalDatumExtractor; import cwms.cda.data.dao.VerticalDatum; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.StatusResponse; +import cwms.cda.data.dto.VerticalDatumInfo; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; @@ -72,9 +75,7 @@ import cwms.cda.formatters.xml.XMLv2; import cwms.cda.helpers.DateUtils; import hec.data.RatingException; -import hec.data.cwmsRating.AbstractRating; import hec.data.cwmsRating.RatingSet; -import hec.data.cwmsRating.TableRating; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.core.validation.JavalinValidation; @@ -91,6 +92,7 @@ import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import javax.xml.transform.TransformerException; + import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; import mil.army.usace.hec.metadata.VerticalDatumException; import org.jetbrains.annotations.NotNull; @@ -140,7 +142,16 @@ protected RatingDao getRatingDao(DSLContext dsl) { required = true), queryParams = { @OpenApiParam(name = STORE_TEMPLATE, type = Boolean.class, - description = "Also store updates to the rating template. Default: true") + description = "Also store updates to the rating template. Default: true"), + @OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided " + + "rating-set includes an explicit vertical-datum-info attribute " + + "then it is assumed that the data is in the datum specified by the vertical-datum-info. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is not provided it is assumed that the data is in the as-stored " + + "datum and no conversion is necessary. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is provided it is assumed that the data is in the Datum named by the argument " + + "and should be converted to the as-stored datum before being saved.") }, method = HttpMethod.POST, path = "/ratings", tags = {TAG}, responses = { @@ -153,7 +164,10 @@ public void create(@NotNull Context ctx) { RatingDao ratingDao = getRatingDao(dsl); boolean storeTemplate = ctx.queryParamAsClass(STORE_TEMPLATE, Boolean.class).getOrDefault(true); String ratingSet = deserializeRatingSet(ctx, storeTemplate); - ratingDao.create(ratingSet, false); + VerticalDatum vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) + .getOrDefault(null); + vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd); + ratingDao.create(ratingSet, false, vd); StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Rating Set successfully stored to CWMS."); ctx.status(HttpServletResponse.SC_CREATED).json(re); } catch (IOException ex) { @@ -453,16 +467,27 @@ private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod meth ratingSet.toNativeVerticalDatum(); break; default: - logger.log(Level.SEVERE, "Unknown vertical datum: " + verticalDatum); + logger.atSevere().log("Unknown vertical datum: %s", verticalDatum); break; } + VerticalDatumInfo vdi = RatingsVerticalDatumExtractor.deserializeVerticalDatumInfoXml(ratingSet.getVerticalDatumInfo()); + if(vdi != null && vdi.getOffsetForDatum(verticalDatum) != null) { + VerticalDatumInfo newVdi = vdi.convertedTo(vdi.getOffsetForDatum(verticalDatum)); + XmlMapper xmlMapper = new XmlMapper(); + String vdiXml = xmlMapper.writeValueAsString(newVdi); + ratingSet.setVerticalDatumInfo(vdiXml); + } } catch (VerticalDatumException vde) { - logger.log(Level.WARNING, vde, () -> "Failed to convert rating " + rating + " to requested vertical datum: " + verticalDatum); + logger.atWarning().withCause(vde).log("Failed to convert rating %s to requested vertical datum: %s", + rating, verticalDatum); } } if (isJson) { retval = JsonRatingUtils.toJson(ratingSet); } else { + //the toXml method in RatingXmlFactory converts to native-datum which breaks things coming back in the user-requested datum + //setting the current-datum to an unknown value prevents the call to convert to native-datum + ratingSet.getVerticalDatumContainer().currentDatum = "ignoreConversionToNativeDatum"; retval = RatingXmlFactory.toXml(ratingSet, " "); } } else { @@ -522,7 +547,16 @@ private RatingSet getRatingSet(Context ctx, RatingSet.DatabaseLoadMethod method, @OpenApiParam(name = STORE_TEMPLATE, type = Boolean.class, description = "Also store updates to the rating template. Default: true"), @OpenApiParam(name = REPLACE_BASE_CURVE, type = Boolean.class, - description = "Replace the base curve of USGS stream flow rating. Default: false") + description = "Replace the base curve of USGS stream flow rating. Default: false"), + @OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided " + + "rating-set includes an explicit vertical-datum-info attribute " + + "then it is assumed that the data is in the datum specified by the vertical-datum-info. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is not provided it is assumed that the data is in the as-stored " + + "datum and no conversion is necessary. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is provided it is assumed that the data is in the Datum named by the argument " + + "and should be converted to the as-stored datum before being saved.") }, method = HttpMethod.PATCH, path = "/ratings", tags = {TAG}) public void update(@NotNull Context ctx, @NotNull String ratingId) { @@ -537,7 +571,10 @@ public void update(@NotNull Context ctx, @NotNull String ratingId) { boolean replaceBaseCurve = ctx.queryParamAsClass(REPLACE_BASE_CURVE, Boolean.class) .getOrDefault(false); String ratingSet = deserializeRatingSet(ctx, storeTemplate); - ratingDao.store(ratingSet, replaceBaseCurve); + VerticalDatum vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) + .getOrDefault(null); + vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd); + ratingDao.store(ratingSet, replaceBaseCurve, vd); StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Updated RatingSet"); ctx.status(HttpServletResponse.SC_OK).json(re); } catch (IOException ex) { 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 5577cc11c..5cb7ddf5e 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 @@ -51,6 +51,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -72,6 +73,7 @@ import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; @@ -215,6 +217,34 @@ protected static Double toDouble(BigDecimal bigDecimal) { return retVal; } + /** + * The idea here is that this will check the current default datum, + * possible switch to the specified datum and + * then run the code and + * if the datum was previously switched + * then switch back to the initial datum. + * @param targetDatum The desired ver + * @param dslContext + * @param cr + */ + protected void withDefaultDatum(@Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { + String defaultVertDatum = CWMS_LOC_PACKAGE.call_GET_DEFAULT_VERTICAL_DATUM(dslContext.configuration()); + String targetName = (targetDatum != null) ? targetDatum.toString() : null; + boolean changeDefaultDatum = !Objects.equals(targetName, defaultVertDatum); + try { + if (changeDefaultDatum) { + CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), targetName); + } + + connection(dslContext, cr); + } finally { + if (changeDefaultDatum) { + // If we changed it we should restore. + CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), defaultVertDatum); + } + } + } + /** * Oracle supports case insensitive regexp search but the syntax for calling it is a * bit weird. This method lets Dao classes add a case-insensitive regexp search in diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java index e69a0944c..8e106c4ae 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java @@ -35,7 +35,7 @@ public interface RatingDao { Pattern officeMatcher = Pattern.compile(".*office-id=\"(.*?)\""); - void create(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException; + void create(String ratingSet, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException; RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, String specificationId, Instant start, Instant end) throws IOException, RatingException; @@ -46,10 +46,18 @@ String retrieveRatings(String format, String names, String unit, String datum, S String start, String end, String timezone); - void store(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException; + void store(String ratingSet, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException; void delete(String officeId, String specificationId, Instant start, Instant end); + default void create(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException { + create(ratingSet, replaceBaseCurve, null); + } + + default void store(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException { + store(ratingSet, replaceBaseCurve, null); + } + static String extractOfficeFromXml(String xml) { Matcher officeMatch = officeMatcher.matcher(xml); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 8a7b60a10..146a961e0 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -48,25 +48,8 @@ public RatingSetDao(DSLContext dsl) { } @Override - public void create(String ratingSetXml, boolean replaceBaseCurve) throws IOException, RatingException { - try { - connection(dsl, c -> { - // can't exist if we are creating, if it exists use store - String office = extractOfficeId(ratingSetXml); - DSLContext context = getDslContext(c, office); - String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), - ratingSetXml, "T", replaceBaseCurve ? "T" : "F"); - if (errs != null && !errs.isEmpty()) { - throw new DataAccessException("Failed to create Rating", new RatingException(errs)); - } - }); - } catch (DataAccessException ex) { - Throwable cause = ex.getCause(); - if (cause instanceof RatingException) { - throw (RatingException) cause; - } - throw new IOException("Failed to create Rating", ex); - } + public void create(String ratingSetXml, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException { + connection(dsl, connection -> storeWithDefaultDatum(ratingSetXml, replaceBaseCurve, true, vd, connection)); } private static String extractOfficeId(String ratingSet) throws JsonProcessingException { @@ -116,9 +99,11 @@ public RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, RatingSet.DatabaseLoadMethod finalMethod = method; - connection(dsl, c -> retval[0] = - RatingJdbcFactory.ratingSet(finalMethod, new RatingConnectionProvider(c), officeId, - specificationId, start, end, false)); + connection(dsl, c -> { + setOffice(c, officeId); + retval[0] = RatingJdbcFactory.ratingSet(finalMethod, new RatingConnectionProvider(c), officeId, + specificationId, start, end, false); + }); } catch (DataAccessException ex) { @@ -137,18 +122,20 @@ public RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, // store/update @Override - public void store(String ratingSetXml, boolean replaceBaseCurve) throws IOException, RatingException { + public void store(String ratingSetXml, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException { + connection(dsl, connection -> storeWithDefaultDatum(ratingSetXml, replaceBaseCurve, false, vd, connection)); + } + + private static void storeRatingSetXml(String ratingSetXml, boolean replaceBaseCurve, boolean failIfExists, Connection c) throws RatingException, IOException { try { - connection(dsl, c -> { - String office = extractOfficeId(ratingSetXml); - DSLContext context = getDslContext(c, office); - String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), - ratingSetXml, "F", replaceBaseCurve ? "T" : "F"); - if (errs != null && !errs.isEmpty()) - { - throw new DataAccessException("Failed to store Rating", new RatingException(errs)); - } - }); + String office = extractOfficeId(ratingSetXml); + DSLContext context = getDslContext(c, office); + String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), + ratingSetXml, formatBool(failIfExists), formatBool(replaceBaseCurve)); + if (errs != null && !errs.isEmpty()) + { + throw new DataAccessException("Failed to store Rating", new RatingException(errs)); + } } catch (DataAccessException ex) { Throwable cause = ex.getCause(); if (cause instanceof RatingException) { @@ -158,6 +145,13 @@ public void store(String ratingSetXml, boolean replaceBaseCurve) throws IOExcept } } + private void storeWithDefaultDatum(String ratingSetXml, boolean replaceBaseCurve, boolean failIfExists, + VerticalDatum vd, Connection connection) throws Throwable { + String office = extractOfficeId(ratingSetXml); + DSLContext dslContext = getDslContext(connection, office); + withDefaultDatum(vd, dslContext, (conn)-> storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, connection)); + } + @Override public void delete(String officeId, String specificationId, Instant start, Instant end) { Timestamp startDate = new Timestamp(start.toEpochMilli()); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java new file mode 100644 index 000000000..1b60bbdf1 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java @@ -0,0 +1,60 @@ +package cwms.cda.data.dao; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import cwms.cda.data.dto.VerticalDatumInfo; + +import java.util.List; +import java.util.Optional; + +public class RatingsVerticalDatumExtractor { + + private RatingsVerticalDatumExtractor() { + throw new AssertionError("Utility class, don't instantiate"); + } + + public static Optional getVerticalDatum(String ratingSet) { + return Optional.ofNullable(ratingSet) + .flatMap(RatingsVerticalDatumExtractor::getVerticalDatumInfo) + .map(VerticalDatumInfo::getNativeDatum) + .filter(s -> !s.isEmpty()) + .map(s -> { + if (s.equalsIgnoreCase(VerticalDatum.OTHER.toString())) { + throw new IllegalArgumentException("Vertical Datum of OTHER is not currently supported."); + } + return VerticalDatum.getVerticalDatum(s); + }); + } + + public static Optional getVerticalDatumInfo(String ratingSet) { + try { + return extractVerticalDatumInfo(ratingSet).map(RatingsVerticalDatumExtractor::deserializeVerticalDatumInfoXml); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse Vertical Datum Info", e); + } + } + + public static VerticalDatumInfo deserializeVerticalDatumInfoXml(String vdiXml) { + XmlMapper xmlMapper = new XmlMapper(); + try { + return xmlMapper.readValue(vdiXml, VerticalDatumInfo.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse Vertical Datum Info", e); + } + } + + private static Optional extractVerticalDatumInfo(String ratingSet) throws JsonProcessingException { + XmlMapper xmlMapper = new XmlMapper(); + JsonNode node = xmlMapper.readTree(ratingSet); + List values = node.findValues("vertical-datum-info"); + Optional retVal = Optional.empty(); + if (!values.isEmpty()) { + JsonNode vdiNode = values.get(values.size() - 1); + retVal = Optional.ofNullable(xmlMapper.writer() + .withRootName("vertical-datum-info") + .writeValueAsString(vdiNode)); + } + return retVal; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index 9230d22e7..bca30d2b1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -1476,36 +1476,6 @@ public void create(TimeSeries input, }); } - // - - /** - * The idea here is that this will check the current default datum, - * possible switch to the specified datum and - * then run the code and - * if the datum was previously switched - * then switch back to the initial datum. - * @param targetDatum The desired ver - * @param dslContext - * @param cr - */ - private void withDefaultDatum(@Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { - String defaultVertDatum = CWMS_LOC_PACKAGE.call_GET_DEFAULT_VERTICAL_DATUM(dslContext.configuration()); - String targetName = (targetDatum != null) ? targetDatum.toString() : null; - boolean changeDefaultDatum = !Objects.equals(targetDatum, defaultVertDatum); - try { - if (changeDefaultDatum) { - CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), targetName); - } - - connection(dslContext, cr); - }finally{ - if (changeDefaultDatum) { - // If we changed it we should restore. - CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), defaultVertDatum); - } - } - } - @Override public void store(TimeSeries timeSeries, Timestamp versionDate) { store(timeSeries, false, StoreRule.REPLACE_ALL, TimeSeriesDaoImpl.OVERRIDE_PROTECTION, null); 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 e4369a3cb..761d748f9 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 @@ -7,10 +7,15 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import cwms.cda.data.dao.VerticalDatum; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @JsonRootName("vertical-datum-info") @JsonDeserialize(builder = VerticalDatumInfo.Builder.class) @@ -31,6 +36,8 @@ public class VerticalDatumInfo extends CwmsDTOBase { // Serialize empty arrays in the xml @JsonInclude(JsonInclude.Include.ALWAYS) + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "offset") VerticalDatumInfo.Offset[] offsets = new Offset[0]; private VerticalDatumInfo() { @@ -100,13 +107,24 @@ private VerticalDatumInfo.Offset[] buildConvertedOffsets(VerticalDatum convertTo //add the other offsets, adjusted VerticalDatumInfo.Offset[] offsets = getOffsets(); + //if contains a zero offset, we will mimic that for the converted datum by adding a zero offset (the datum we converted to) + boolean hasZeroOffset = Arrays.stream(offsets) + .anyMatch(offset -> offset.getValue() == 0.0); for (VerticalDatumInfo.Offset offset : offsets) { String toDatum = offset.getToDatum(); - if (!offset.isForDatum(convertTo.toString())) { - Double newOffsetValue = convertToOffsetToOriginal + offset.getValue(); - boolean isEstimate = offset.isEstimate() || convertToOffset.isEstimate(); - VerticalDatumInfo.Offset newOffset = new VerticalDatumInfo.Offset(isEstimate, toDatum, newOffsetValue); - newOffsets.add(newOffset); + Set existingDatums = newOffsets.stream().map(Offset::getToDatum) + .collect(Collectors.toSet()); + if(!existingDatums.contains(offset.getToDatum())) { + if (!offset.isForDatum(convertTo.toString())) { + Double newOffsetValue = convertToOffsetToOriginal + offset.getValue(); + boolean isEstimate = offset.isEstimate() || convertToOffset.isEstimate(); + VerticalDatumInfo.Offset newOffset = new VerticalDatumInfo.Offset(isEstimate, toDatum, newOffsetValue); + newOffsets.add(newOffset); + } else if(hasZeroOffset) { + //this is the one we converted to, its now zero offset + VerticalDatumInfo.Offset newOffset = new VerticalDatumInfo.Offset(false, toDatum, 0.0); + newOffsets.add(newOffset); + } } } return newOffsets.toArray(new VerticalDatumInfo.Offset[]{}); @@ -231,6 +249,8 @@ public VerticalDatumInfo.Builder withElevation(Double elevation) { return this; } + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "offset") public VerticalDatumInfo.Builder withOffsets(VerticalDatumInfo.Offset[] offsets) { this.offsets = offsets; return this; diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java deleted file mode 100644 index a12500618..000000000 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/DeleteMeTest.java +++ /dev/null @@ -1,51 +0,0 @@ -//package cwms.cda.api.rating; -// -//import cwms.cda.data.dao.VerticalDatum; -//import hec.data.cwmsRating.RatingSet; -//import hec.data.cwmsRating.io.RatingSetContainer; -//import java.io.File; -//import java.io.IOException; -//import java.net.URL; -//import java.nio.file.Files; -//import java.nio.file.Path; -//import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; -//import org.jetbrains.annotations.NotNull; -//import org.junit.jupiter.api.Test; -//import static org.junit.jupiter.api.Assertions.*; -// -//public class DeleteMeTest { -// static final String BASE_LOCATION = "RatingDatumTest"; -// static final String LOC_WITH_NAVD88 = BASE_LOCATION + "-NAVD88"; -// static final String LOC_WITH_NGVD29 = BASE_LOCATION + "-NGVD29"; -// -// protected static String readResourceFile(String resourcePath) throws IOException { -// URL resource = DeleteMeTest.class.getClassLoader().getResource(resourcePath); -// if (resource == null) { -// throw new IOException("Resource not found: " + resourcePath); -// } -// Path path = new File(resource.getFile()).toPath(); -// return String.join("\n", Files.readAllLines(path)); -// } -// -// static @NotNull String readVerticalDatumRatingXml(String location) throws IOException { -// return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml").replace("{office-id}", "SPK") -// .replace("{location}", location); -// } -// -// @Test -// void test() throws Exception { -// String xml = readVerticalDatumRatingXml(LOC_WITH_NGVD29); -// RatingSetContainer container = RatingSetContainerXmlFactory.ratingSetContainerFromXml(xml); -// RatingSet rs = new RatingSet(container); -// RatingSet rsNew = new RatingSet(container); -// -// assertTrue(rs.toNAVD88()); -// assertNotEquals(rs.getRatings()[0].getRatingExtents()[0][0], rsNew.getRatings()[0].getRatingExtents()[0][0], 0.01); -// -// VerticalDatum vd = VerticalDatum.getVerticalDatum(rs.getNativeVerticalDatum()); -// assertEquals(VerticalDatum.NGVD29, vd); -// -// vd = VerticalDatum.getVerticalDatum(rs.getCurrentVerticalDatum()); -// assertEquals(VerticalDatum.NAVD88, vd); -// } -//} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java index 9de4b9f90..1acbb4f58 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java @@ -20,9 +20,12 @@ package cwms.cda.api.rating; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import cwms.cda.api.DataApiTestIT; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.VerticalDatum; +import cwms.cda.data.dto.VerticalDatumInfo; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; import hec.data.cwmsRating.AbstractRating; @@ -43,11 +46,13 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -78,6 +83,253 @@ static void beforeAll() throws Exception { createSpec(specXml, user); } + @Test + void test_create_with_datum_param_differs_from_location_native_datum() throws Exception { + // Verify RatingsController create (POST /ratings) accepts the datum query parameter + // when the input rating XML does not include vertical-datum-info. + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Build rating XML for BASE_LOCATION and strip datum info + String xmlWithDatum = readVerticalDatumRatingXml(LOC_WITH_NGVD29); + String xml = stripVerticalDatumInfo(xmlWithDatum); + + RatingSet ratingSet = RatingXmlFactory.ratingSet(xmlWithDatum); + String ratingId = ratingSet.getRatingSpec().getRatingSpecId(); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlWithDatum); + JsonNode firstIndNode = root + .path("simple-rating") + .path("rating-points") + .path("point") + .get(0) + .path("ind"); + double firstElev = firstIndNode.asDouble(); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(VerticalDatum.NAVD88); + + // First create with a datum that doesn't match native datum for location + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) //using xml with no datum info so param is used + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // 2) verify elevation is as expected + Response response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev + offset.getValue(), 0.001)); + + deleteRatingEffectiveDates(user, ratingId); + } + + @Test + void test_update_datum_not_in_body_uses_param() throws Exception { + // Verify RatingsController update/store (PATCH /ratings/{id}) accepts the datum query parameter + // when the input rating XML does not include vertical-datum-info. + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Build rating XML for BASE_LOCATION and strip datum info + String xmlWithDatum = readVerticalDatumRatingXml(LOC_WITH_NGVD29); + String xml = stripVerticalDatumInfo(xmlWithDatum); + + RatingSet ratingSet = RatingXmlFactory.ratingSet(xmlWithDatum); + String ratingId = ratingSet.getRatingSpec().getRatingSpecId(); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlWithDatum); + JsonNode firstIndNode = root + .path("simple-rating") + .path("rating-points") + .path("point") + .get(0) + .path("ind"); + double firstElev = firstIndNode.asDouble(); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(VerticalDatum.NAVD88); + + // First POST to ensure the rating exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xmlWithDatum) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // 2) verify elevation is as expected + Response response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev, 0.001)); + + + // 3) PATCH with datum = NAVD88 and no datum info in body means we apply offset, so value changes + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)); + + // 4) retrieve rating and verify value is changed as expected + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev + offset.getValue(), 0.001)); + + deleteRatingEffectiveDates(user, ratingId); + } + + @Test + void test_update_with_datum_in_body_ignores_param() throws Exception { + // Verify RatingsController update/store (PATCH /ratings/{id}) ignores the datum query parameter + // when the input rating XML does include vertical-datum-info. + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Build rating XML for BASE_LOCATION and strip datum info + String xmlWithDatum = readVerticalDatumRatingXml(LOC_WITH_NGVD29); + + RatingSet ratingSet = RatingXmlFactory.ratingSet(xmlWithDatum); + String ratingId = ratingSet.getRatingSpec().getRatingSpecId(); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlWithDatum); + JsonNode firstIndNode = root + .path("simple-rating") + .path("rating-points") + .path("point") + .get(0) + .path("ind"); + double firstElev = firstIndNode.asDouble(); + + // First POST to ensure the rating exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xmlWithDatum) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // 2) verify elevation is as expected + Response response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev, 0.001)); + + + // 3) PATCH with datum = NAVD88 but it should be ignored since body contains datum info + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xmlWithDatum) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)); + + // 4) retrieve rating and verify elevation is still as expected + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev, 0.001)); + + deleteRatingEffectiveDates(user, ratingId); + } + private static void createSpec(String specXml, TestAccounts.KeyUser user) { given() .log().ifValidationFails(LogDetail.ALL, true) @@ -142,18 +394,23 @@ static void cleanUp() { @MethodSource(value = "provideDatumCombinations") @ParameterizedTest void test_vertical_datum_get_all(TestLocationIds locId, TestLocationVerticalDatumData testData) throws Exception { - //This tests getting a rating with various combinations of native location datum and requested datum - //Storing a rating without any vertical datum info, then requesting it back with various datum requests String xml = readVerticalDatumRatingXml(locId._locationId); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xml); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + vdi = vdi.convertedTo(vdi.getOffsetForDatum(locId._nativeDatum)); + String newVdiXml = xmlMapper.writeValueAsString(vdi); + xml = xml.replaceAll("", newVdiXml); RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); - TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); AbstractRating originalRating = originalRatingSet.getRatings()[0]; originalRating.setVerticalDatumContainer(null); - VerticalDatum storedVerticalDatum = null; - storeRatingFromXml(xml, user, storedVerticalDatum); + storeRatingFromXml(xml, user); //Request the one rating id we stored, using the getAll endpoint with a query param filter String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); @@ -177,24 +434,6 @@ void test_vertical_datum_get_all(TestLocationIds locId, TestLocationVerticalDatu .extract(); deleteRatingEffectiveDates(user, ratingId); - - RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); - VerticalDatumContainer receivedDatumContainer = receivedRatingSet.getVerticalDatumContainer(); - assertEquals(locId._nativeDatum == null, receivedDatumContainer == null, "Received VerticalDatumContainer presence mismatch. Expected " + (locId._nativeDatum == null ? "null" : "not null")); - - - VerticalDatum expectedDatum = testData._requestedVerticalDatum; - - if (testData._requestedVerticalDatum == VerticalDatum.NATIVE || testData._requestedVerticalDatum == null || locId._nativeDatum == null) { - expectedDatum = locId._nativeDatum; - } - - VerticalDatum receivedDatum = null; - if (receivedDatumContainer != null) { - receivedDatum = VerticalDatum.getVerticalDatum(receivedDatumContainer.getCurrentVerticalDatum()); - } - - assertEquals(expectedDatum, receivedDatum, "Unexpected Current Vertical Datum received"); } @MethodSource(value = "provideDatumCombinations") @@ -203,15 +442,36 @@ void test_vertical_datum_get_one(TestLocationIds locId, TestLocationVerticalDatu //This tests getting a rating with various combinations of native location datum and requested datum //Storing a rating without any vertical datum info, then requesting it back with various datum requests String xml = readVerticalDatumRatingXml(locId._locationId); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xml); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + vdi = vdi.convertedTo(vdi.getOffsetForDatum(locId._nativeDatum)); + String newVdiXml = xmlMapper.writeValueAsString(vdi); + xml = xml.replaceAll("", newVdiXml); RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); - + double firstElev = originalRatingSet.getRatings()[0].getValues(0)[0].getIndValue(); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); AbstractRating originalRating = originalRatingSet.getRatings()[0]; originalRating.setVerticalDatumContainer(null); - VerticalDatum storedVerticalDatum = null; - storeRatingFromXml(xml, user, storedVerticalDatum); + double expectedFirstElev = firstElev; + if (testData._requestedVerticalDatum == VerticalDatum.NATIVE || testData._requestedVerticalDatum == null || locId._nativeDatum == null) { + expectedFirstElev = firstElev; + } + else if(testData._requestedVerticalDatum != locId._nativeDatum) { + //Need to apply offset + + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(testData._requestedVerticalDatum); + //storedValue = NAVD88 + offset + //-> NAVD88 = storedValue - offset + expectedFirstElev = firstElev - offset.getValue(); + } + + storeRatingFromXml(xml, user); //Use getOne endpoint to get the rating we just stored String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); @@ -239,39 +499,28 @@ void test_vertical_datum_get_one(TestLocationIds locId, TestLocationVerticalDatu VerticalDatumContainer receivedDatumContainer = receivedRatingSet.getVerticalDatumContainer(); assertEquals(locId._nativeDatum == null, receivedDatumContainer == null, "Received VerticalDatumContainer presence mismatch. Expected " + (locId._nativeDatum == null ? "null" : "not null")); + double receivedFirstElev = receivedRatingSet.getRatings()[0].getValues(0)[0].getIndValue(); - VerticalDatum expectedDatum = testData._requestedVerticalDatum; - - if (testData._requestedVerticalDatum == VerticalDatum.NATIVE || testData._requestedVerticalDatum == null || locId._nativeDatum == null) { - expectedDatum = locId._nativeDatum; - } - - VerticalDatum receivedDatum = null; - if (receivedDatumContainer != null) { - receivedDatum = VerticalDatum.getVerticalDatum(receivedDatumContainer.getCurrentVerticalDatum()); - } - - assertEquals(expectedDatum, receivedDatum, "Unexpected Current Vertical Datum received"); + assertEquals(expectedFirstElev, receivedFirstElev, "Unexpected elev value received"); } - private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user, VerticalDatum storedVerticalDatum) { + private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user) { given() - .log().ifValidationFails(LogDetail.ALL, true) - .contentType(Formats.XMLV2) - .body(xml) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, SPK) - .queryParam(DATUM, storedVerticalDatum) - .when() - .redirects() - .follow(true) - .redirects() - .max(3) - .post("/ratings") - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL, true) - .statusCode(is(HttpServletResponse.SC_CREATED)); + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); } private static void deleteRatingEffectiveDates(TestAccounts.KeyUser user, String ratingId) { @@ -296,7 +545,6 @@ private static void deleteRatingEffectiveDates(TestAccounts.KeyUser user, String private static Stream provideDatumCombinations() { //This provides information for 3 locations: - // - BASE_LOCATION: no vertical datum // - LOC_WITH_NAVD88: native datum NAVD88 // - LOC_WITH_NGVD29: native datum NGVD29 //And for each location, we test requesting: @@ -305,18 +553,25 @@ private static Stream provideDatumCombinations() { // - NAVD88 // - NGVD29 // - //This creates a 3 x 4 matrix of test cases to cover all combinations of these parameters + //This creates a 2 x 4 matrix of test cases to cover all combinations of these parameters return Stream.of(TestLocationIds.values()) - .flatMap(locId -> Stream.of(TestLocationVerticalDatumData.values()) - .map(datum -> Arguments.of(locId, datum))); + .filter(locId -> !BASE_LOCATION.equals(locId._locationId)) + .flatMap(locId -> Stream.of(TestLocationVerticalDatumData.values()) + .map(datum -> Arguments.of(locId, datum))); } - private static @NotNull String readVerticalDatumRatingXml(String location) throws IOException { + static @NotNull String readVerticalDatumRatingXml(String location) throws IOException { return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml").replace("{office-id}", SPK) .replace("{location}", location); } + // Remove the vertical-datum-info element from the rating XML so that the controller must + // rely on the datum query parameter or the location's native datum. + private static String stripVerticalDatumInfo(String xml) { + return xml.replaceAll("(?s)", ""); + } + private enum TestLocationIds { BASE(BASE_LOCATION, null), NAVD88(LOC_WITH_NAVD88, VerticalDatum.NAVD88), diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java new file mode 100644 index 000000000..f19230fb4 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java @@ -0,0 +1,20 @@ +package cwms.cda.api.rating; + +import cwms.cda.data.dao.RatingsVerticalDatumExtractor; +import cwms.cda.data.dao.VerticalDatum; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RatingsVerticalDatumExtractorTest { + @Test + void testGetVerticalDatum() throws Exception { + String xml = RatingsControllerTestVerticalDatumIT.readVerticalDatumRatingXml(RatingsControllerTestVerticalDatumIT.LOC_WITH_NGVD29); + Optional datum = RatingsVerticalDatumExtractor.getVerticalDatum(xml); + assertTrue(datum.isPresent()); + datum.ifPresent(vd -> assertSame(VerticalDatum.NGVD29, vd)); + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml b/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml index ef7989392..4b9cf1cab 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml +++ b/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml @@ -37,14 +37,14 @@ {location}.Elev;Area.Standard.Production NGVD-29 - 612.99 + 36.089 NGVD-29 0.0 NAVD-88 - 2.393 + -2.532 ft;acre diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e49a183e..13efc0bff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ hec-nucleus = "2.0.1" flogger = "0.7.4" google-findbugs = "3.0.2" error_prone_annotations = "2.15.0" -cwms-ratings = "2.0.2" +cwms-ratings = "4.2.2" javalin = "4.6.8" tomcat = "9.0.112" swagger-core = "2.2.23" From a8517a0555fac6b4a48701f9cfaa390f0bcb8126 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Wed, 7 Jan 2026 23:42:34 -0800 Subject: [PATCH 08/16] CDA-46 - Removed unused vertical datum constant --- cwms-data-api/src/main/java/cwms/cda/api/Controllers.java | 1 - 1 file changed, 1 deletion(-) 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 d68bee3d4..3528a22e7 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 @@ -96,7 +96,6 @@ public final class Controllers { public static final String NAME = "name"; public static final String CASCADE_DELETE = "cascade-delete"; public static final String DATUM = "datum"; - public static final String VERTICAL_DATUM = "vertical-datum"; public static final String SINCE = "since"; public static final String BEGIN = "begin"; public static final String END = "end"; From d6d20ba7601044a319f29c19b546e89d081dc9c0 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 8 Jan 2026 11:36:51 -0800 Subject: [PATCH 09/16] CDA-46 - Adds setting of local datum for conversions to work similar to setting default vert datum. --- .../cwms/cda/api/rating/RatingController.java | 4 +- .../java/cwms/cda/data/dao/RatingSetDao.java | 53 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 075a0dfd1..2243603b6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -487,7 +487,9 @@ private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod meth } else { //the toXml method in RatingXmlFactory converts to native-datum which breaks things coming back in the user-requested datum //setting the current-datum to an unknown value prevents the call to convert to native-datum - ratingSet.getVerticalDatumContainer().currentDatum = "ignoreConversionToNativeDatum"; + if(ratingSet.getVerticalDatumContainer() != null && ratingSet.getVerticalDatumContainer().currentDatum != null) { + ratingSet.getVerticalDatumContainer().currentDatum = "ignoreConversionToNativeDatum"; + } retval = RatingXmlFactory.toXml(ratingSet, " "); } } else { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 146a961e0..4b0c68c15 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -27,6 +27,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.data.dto.rating.RatingSpec; import hec.data.RatingException; import hec.data.cwmsRating.RatingSet; import java.io.IOException; @@ -34,10 +36,14 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.List; + import mil.army.usace.hec.cwms.rating.io.jdbc.ConnectionProvider; import mil.army.usace.hec.cwms.rating.io.jdbc.RatingJdbcFactory; +import org.jetbrains.annotations.Nullable; +import org.jooq.ConnectionRunnable; import org.jooq.DSLContext; import org.jooq.exception.DataAccessException; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_RATING_PACKAGE; @@ -64,6 +70,18 @@ private static String extractOfficeId(String ratingSet) throws JsonProcessingExc return office; } + private static String extractLocationId(String ratingSet) throws JsonProcessingException { + XmlMapper xmlMapper = new XmlMapper(); + JsonNode node = xmlMapper.readTree(ratingSet); + List values = node.findValues("location-id"); + String location = ""; + if (!values.isEmpty()) { + //Getting the last instance since the order is template, spec, rating + location = values.get(values.size() - 1).textValue(); + } + return location; + } + @Override public String retrieveLatestXML(String officeId, String specificationId) { return connectionResult(dsl, c -> { @@ -148,8 +166,41 @@ private static void storeRatingSetXml(String ratingSetXml, boolean replaceBaseCu private void storeWithDefaultDatum(String ratingSetXml, boolean replaceBaseCurve, boolean failIfExists, VerticalDatum vd, Connection connection) throws Throwable { String office = extractOfficeId(ratingSetXml); + String locationId = extractLocationId(ratingSetXml); DSLContext dslContext = getDslContext(connection, office); - withDefaultDatum(vd, dslContext, (conn)-> storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, connection)); + withLocalAndDefaultDatum(locationId, office, vd, dslContext, c -> storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, c)); + } + + protected void withLocalAndDefaultDatum(String locationId, String officeId, @Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { + boolean localDatumAdded = false; + try { + //if converting to NAVD88 or NGVD29, we need to set the local datum to the native datum temporarily or the conversion will fail in the db + if(targetDatum == VerticalDatum.NAVD88 || targetDatum == VerticalDatum.NGVD29) { + String vertDatum = CWMS_LOC_PACKAGE.call_GET_VERTICAL_DATUM_INFO_F__2(dslContext.configuration(), locationId, "m", officeId); + if(vertDatum != null) + { + XmlMapper xmlMapper = new XmlMapper(); + VerticalDatumInfo vdi = xmlMapper.readValue(vertDatum, VerticalDatumInfo.class); + String nativeDatum = vdi.getNativeDatum(); + // Only set local datum temporarily if native datum is NAVD88 or NGVD29 to allow conversion + // If native datum is unknown for some reason then just set to the target datum since there is no conversion needed anyways + if(VerticalDatum.NAVD88.toString().equals(nativeDatum) || VerticalDatum.NGVD29.toString().equals(nativeDatum)) { + CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, nativeDatum, "T", officeId); + localDatumAdded = true; + } else if(nativeDatum == null || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { + CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, targetDatum.toString(), "T", officeId); + localDatumAdded = true; + } + } + } + withDefaultDatum(targetDatum, dslContext, cr); + } catch (IOException e) { + throw new DataAccessException("Failed to parse vertical datum info for location " + locationId, e); + } finally { + if(localDatumAdded) { + CWMS_LOC_PACKAGE.call_DELETE_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, officeId); + } + } } @Override From ae9b9b3abee9a81b4d214618cd735b528592e757 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 8 Jan 2026 13:15:24 -0800 Subject: [PATCH 10/16] CDA-46 - Fixed validation error on datum param if its empty --- .../cwms/cda/api/rating/RatingController.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 2243603b6..24dd660c5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -164,8 +164,12 @@ public void create(@NotNull Context ctx) { RatingDao ratingDao = getRatingDao(dsl); boolean storeTemplate = ctx.queryParamAsClass(STORE_TEMPLATE, Boolean.class).getOrDefault(true); String ratingSet = deserializeRatingSet(ctx, storeTemplate); - VerticalDatum vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) - .getOrDefault(null); + String datum = ctx.queryParam(DATUM); + VerticalDatum vd = null; + if(datum != null) { + vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) + .getOrDefault(null); + } vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd); ratingDao.create(ratingSet, false, vd); StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Rating Set successfully stored to CWMS."); @@ -573,8 +577,12 @@ public void update(@NotNull Context ctx, @NotNull String ratingId) { boolean replaceBaseCurve = ctx.queryParamAsClass(REPLACE_BASE_CURVE, Boolean.class) .getOrDefault(false); String ratingSet = deserializeRatingSet(ctx, storeTemplate); - VerticalDatum vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) - .getOrDefault(null); + String datum = ctx.queryParam(DATUM); + VerticalDatum vd = null; + if(datum != null) { + vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) + .getOrDefault(null); + } vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd); ratingDao.store(ratingSet, replaceBaseCurve, vd); StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Updated RatingSet"); From 83c165a1557bf25c6163f3024b107a04d6af11ac Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 8 Jan 2026 14:51:15 -0800 Subject: [PATCH 11/16] CDA-46 - Updated datum check to handle dashes. --- cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 4b0c68c15..1c706ccd5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -184,7 +184,7 @@ protected void withLocalAndDefaultDatum(String locationId, String officeId, @Nul String nativeDatum = vdi.getNativeDatum(); // Only set local datum temporarily if native datum is NAVD88 or NGVD29 to allow conversion // If native datum is unknown for some reason then just set to the target datum since there is no conversion needed anyways - if(VerticalDatum.NAVD88.toString().equals(nativeDatum) || VerticalDatum.NGVD29.toString().equals(nativeDatum)) { + if(VerticalDatum.NAVD88 == VerticalDatum.getVerticalDatum(nativeDatum) || VerticalDatum.NGVD29 == VerticalDatum.getVerticalDatum(nativeDatum)) { CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, nativeDatum, "T", officeId); localDatumAdded = true; } else if(nativeDatum == null || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { From 69c6d02a25b26bd16459588facd19eedffd7d074 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 8 Jan 2026 15:58:37 -0800 Subject: [PATCH 12/16] CDA-46 - updated to check for "unknown" datum first to prevent exception --- .../src/main/java/cwms/cda/data/dao/RatingSetDao.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 1c706ccd5..4b00a9d46 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -184,12 +184,12 @@ protected void withLocalAndDefaultDatum(String locationId, String officeId, @Nul String nativeDatum = vdi.getNativeDatum(); // Only set local datum temporarily if native datum is NAVD88 or NGVD29 to allow conversion // If native datum is unknown for some reason then just set to the target datum since there is no conversion needed anyways - if(VerticalDatum.NAVD88 == VerticalDatum.getVerticalDatum(nativeDatum) || VerticalDatum.NGVD29 == VerticalDatum.getVerticalDatum(nativeDatum)) { - CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, nativeDatum, "T", officeId); - localDatumAdded = true; - } else if(nativeDatum == null || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { + if(nativeDatum == null || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, targetDatum.toString(), "T", officeId); localDatumAdded = true; + } else if(VerticalDatum.NAVD88 == VerticalDatum.getVerticalDatum(nativeDatum) || VerticalDatum.NGVD29 == VerticalDatum.getVerticalDatum(nativeDatum)) { + CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, nativeDatum, "T", officeId); + localDatumAdded = true; } } } From 8ac297ca5daaadc29563e67a217a4ab1260819ea Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 8 Jan 2026 16:25:19 -0800 Subject: [PATCH 13/16] CDA-46 - Reverted RatingsControllerTestIT change that uses vertical datum location since we test that in its own test class. --- .../cda/api/rating/RatingsControllerTestIT.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java index 3092c3ac2..3a956f82e 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java @@ -26,7 +26,6 @@ import cwms.cda.api.DataApiTestIT; import cwms.cda.data.dao.JooqDao; -import cwms.cda.data.dao.VerticalDatum; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; import hec.data.cwmsRating.io.RatingSetContainer; @@ -34,17 +33,17 @@ import io.restassured.filter.log.LogDetail; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import java.io.IOException; -import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.cwms.rating.io.xml.RatingContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; + +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; @@ -89,7 +88,7 @@ static void cleanUp() static void store(boolean storeTemplate) throws Exception { //Make sure we always have something. - createLocationWithVerticalDatum(EXISTING_LOC, true, SPK, VerticalDatum.NAVD88); + createLocation(EXISTING_LOC, true, SPK); String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); ratingXml = ratingXml.replaceAll("Zanesville", EXISTING_LOC); @@ -417,5 +416,6 @@ void test_1206_rating_create_xml() throws IOException { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_CREATED)); } + } From 4ddc97311ca6889b5cd77268ef9ebff052c858ec Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 8 Jan 2026 16:33:47 -0800 Subject: [PATCH 14/16] CDA-46 - Adds check to ignore vertical datum conversion if no target vertical datum is passed in. --- .../src/main/java/cwms/cda/data/dao/RatingSetDao.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 4b00a9d46..1fcd17920 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -168,7 +168,13 @@ private void storeWithDefaultDatum(String ratingSetXml, boolean replaceBaseCurve String office = extractOfficeId(ratingSetXml); String locationId = extractLocationId(ratingSetXml); DSLContext dslContext = getDslContext(connection, office); - withLocalAndDefaultDatum(locationId, office, vd, dslContext, c -> storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, c)); + if(vd != null) { + withLocalAndDefaultDatum(locationId, office, vd, dslContext, c -> storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, c)); + } + else { + storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, connection); + } + } protected void withLocalAndDefaultDatum(String locationId, String officeId, @Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { From 2aa15ec6ce3a23731fa2d00134d4dd0793ac1068 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Fri, 9 Jan 2026 12:57:28 -0800 Subject: [PATCH 15/16] CDA-46 - Fixes failing test due to location not existing --- .../src/main/java/cwms/cda/data/dao/RatingSetDao.java | 2 +- .../java/cwms/cda/api/rating/RatingsControllerTestIT.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 1fcd17920..48309a622 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -190,7 +190,7 @@ protected void withLocalAndDefaultDatum(String locationId, String officeId, @Nul String nativeDatum = vdi.getNativeDatum(); // Only set local datum temporarily if native datum is NAVD88 or NGVD29 to allow conversion // If native datum is unknown for some reason then just set to the target datum since there is no conversion needed anyways - if(nativeDatum == null || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { + if(nativeDatum == null || nativeDatum.isBlank() || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, targetDatum.toString(), "T", officeId); localDatumAdded = true; } else if(VerticalDatum.NAVD88 == VerticalDatum.getVerticalDatum(nativeDatum) || VerticalDatum.NGVD29 == VerticalDatum.getVerticalDatum(nativeDatum)) { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java index 3a956f82e..099989698 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java @@ -373,7 +373,7 @@ enum GetAllTest void test_1206_rating_create_json() throws IOException { // example from 1206 but office changed to SPK String body = readResourceFile("cwms/cda/api/spk/ratings_ind.json"); - + body = body.replaceAll("Barren", EXISTING_LOC); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; // Create the set @@ -397,7 +397,7 @@ void test_1206_rating_create_json() throws IOException { void test_1206_rating_create_xml() throws IOException { // example from 1206 but office changed to SPK and converted to xml String body = readResourceFile("cwms/cda/api/spk/ratings_ind.xml"); - + body = body.replaceAll("Barren", EXISTING_LOC); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; // Create the set From f85abd083bf56c720c7be0dd00833a66f6316716 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 22 Jan 2026 13:29:25 -0800 Subject: [PATCH 16/16] CDA-46 - integrating cwms-rating with matching jooq-codegen version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13efc0bff..b221e769b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ hec-nucleus = "2.0.1" flogger = "0.7.4" google-findbugs = "3.0.2" error_prone_annotations = "2.15.0" -cwms-ratings = "4.2.2" +cwms-ratings = "4.2.3" javalin = "4.6.8" tomcat = "9.0.112" swagger-core = "2.2.23"