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 346030bc6..ff4c46c6b 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 @@ -112,6 +112,7 @@ public final class Controllers { public static final String REPLACE_BASE_CURVE = "replace-base-curve"; public static final String EFFECTIVE_DATE_EXACT = "use-exact-effective-date"; + public static final String TIMESERIES_ID_REGEX = "timeseries-id-regex"; public static final String TIMESERIES_ID = "timeseries-id"; public static final String SNAP_FORWARD = "snap-forward"; 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 ab718255e..e8f0eb661 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; @@ -90,6 +93,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; @@ -267,7 +271,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" @@ -364,6 +370,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 = { @@ -378,6 +394,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 = VerticalDatum.getVerticalDatum(ctx.queryParam(DATUM)); Instant beginInstant = null; String begin = ctx.queryParam(BEGIN); @@ -395,7 +412,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); @@ -407,7 +424,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")) { @@ -422,6 +439,28 @@ 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) { + 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) { retval = JsonRatingUtils.toJson(ratingSet); } else { 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 new file mode 100644 index 000000000..2a72044f2 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java @@ -0,0 +1,30 @@ +package cwms.cda.data.dao; + +public enum VerticalDatum +{ + NAVD88("NAVD88"), + NGVD29("NGVD29"), + NATIVE("NATIVE"); + + private final String rule; + + VerticalDatum(String rule) { + this.rule = rule; + } + + public static VerticalDatum getVerticalDatum(String input) { + VerticalDatum retval = null; + + if (input != null && !input.isBlank()) { + input = input.trim().replace("-", ""); + retval = VerticalDatum.valueOf(input.toUpperCase()); + } + return retval; + } + + @Override + public String toString() { + return rule; + } + +} \ No newline at end of file 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 78c42fce0..aa79c7312 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 @@ -32,6 +32,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; @@ -80,6 +81,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; /** @@ -326,6 +328,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..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,6 +26,7 @@ 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; @@ -33,17 +34,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.*; +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; @@ -88,7 +89,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); @@ -416,6 +417,5 @@ void test_1206_rating_create_xml() throws IOException { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_CREATED)); } - } 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..9de4b9f90 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java @@ -0,0 +1,348 @@ +/* + * 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 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; +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.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; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Tag("integration") +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"; + + @BeforeAll + static void beforeAll() throws Exception { + //Make sure we always have something. + createLocation(BASE_LOCATION, true, SPK); + createLocationWithVerticalDatum(LOC_WITH_NAVD88, true, SPK, VerticalDatum.NAVD88); + createLocationWithVerticalDatum(LOC_WITH_NGVD29, true, SPK, VerticalDatum.NGVD29); + + 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); + + 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) + .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)); + } + + @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)); + } + + @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); + 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); + + //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) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .queryParam(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()); + 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"); + } + + 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 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))); + } + + + 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(null), + NATIVE(VerticalDatum.NATIVE), + NAVD88(VerticalDatum.NAVD88), + NGVD29(VerticalDatum.NGVD29), + ; + + final VerticalDatum _requestedVerticalDatum; + + TestLocationVerticalDatumData(VerticalDatum requestedVerticalDatum) { + _requestedVerticalDatum = requestedVerticalDatum; + } + } +} 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 + + + +